Jump to content


Variable parameters retaining type information


19 replies to this topic

#1 eddie

    Senior Member

  • Members
  • PipPipPipPip
  • 751 posts

Posted 23 March 2006 - 08:02 AM

So, I want to have my cake and eat it too.

I'm trying to create a clean binding between my Lua and C++ code, and thanks to tolua++ (and some great help from Ariel Manzur), I think I've just about got it.

The one thing that's irking me, however, is C++ related.

I'd like to have a syntax from C++ that looks like this:

myLuaInterpreterClass.CallLuaFunction("luafunctionname", ...);

Where ... is zero or more parameters, of possibly varying types.

Now, the inside of that function is going to call to various lua functions to set the state and push the actual variable *into* Lua, but it needs to use it's type to determine what to do.

I think I'm hosed, or at least I'll have to do some crazy nasty shit with templates to get it to work. Does anyone have any other ideas? Currently all I'm thinking is something like this:


class LuaInterpreter

{

public:


   void CallLuaFunction(const std::string& funcName);


   template<typename T>

   void CallLuaFunction(const std::string& funcName, const T& param1);


   template<typename T, typename T1>

   void CallLuaFunction(const std::string& funcName, const T& param1, const T1& param2);


   template<typename T, typename T1, typename T2>

   void CallLuaFunction(const std::string& funcName, const T& param1, const T1& param2, const T2& param3);


// ... etc, etc.


};


I've done this before using Boost::PreProcessor, and it's yukky, but it works...

Anyways, perhaps I'm just rambling. Let me know if this makes sense. :D

#2 .oisyn

    DevMaster Staff

  • Moderators
  • 1842 posts

Posted 23 March 2006 - 10:21 AM

That is in fact the only way to do it ;)
C++ addict
-
Currently working on: the 3D engine for Tomb Raider.

#3 Mattias Gustavsson

    Senior Member

  • Members
  • PipPipPipPip
  • 413 posts

Posted 23 March 2006 - 10:43 AM

You could create a "Variant" type, which can store a variable of any type, and then when you call your function, you don't do:

myLuaInterpreterClass.CallLuaFunction("luafunction name", 1.0f, 10, "test");

but rather

myLuaInterpreterClass.CallLuaFunction("luafunction name", Variant(1.0f), Variant(10), Variant("test"));

and then you can use a GetType method in the variant to get which type each parameter is.

The drawback is that if you forget to put Variant() around a parameter somewhere, you'll have a bug (probably even a crash).

I'd still take this before the template version any day :-)

#4 .oisyn

    DevMaster Staff

  • Moderators
  • 1842 posts

Posted 23 March 2006 - 11:01 AM

Mattias Gustavsson said:

You could create a "Variant" type, which can store a variable of any type, and then when you call your function, you don't do:

myLuaInterpreterClass.CallLuaFunction("luafunction name", 1.0f, 10, "test");

but rather

myLuaInterpreterClass.CallLuaFunction("luafunction name", Variant(1.0f), Variant(10), Variant("test"));
That's possible, but keep in mind that copy-ctors won't run for the Variant type, and is therefore very dangerous (if you pass in a std::string for example, and you modify it's contents, it's heap corruption gallore). Also, you don't know how many variables are passed to the function so you'll need an end marker as well (although this information can be retrieved from the lua function that is to be called).

Quote

I'd still take this before the template version any day :-)
Why? The usage is a lot more hassle and it's potentially very harmful if you don't use it in the right way. With templates, you only have the burdon of defining a few functions, but this is a one-time process. And it's type-safe, plus your app won't crash if you forget something.
C++ addict
-
Currently working on: the 3D engine for Tomb Raider.

#5 Mattias Gustavsson

    Senior Member

  • Members
  • PipPipPipPip
  • 413 posts

Posted 23 March 2006 - 02:13 PM

.oisyn said:

Why?

Personal preference :)

#6 .oisyn

    DevMaster Staff

  • Moderators
  • 1842 posts

Posted 23 March 2006 - 03:13 PM

So you prefer typing more error-prone code over safe and clean code? Well it's your choice and I'll respect that ;)
C++ addict
-
Currently working on: the 3D engine for Tomb Raider.

#7 eddie

    Senior Member

  • Members
  • PipPipPipPip
  • 751 posts

Posted 23 March 2006 - 03:18 PM

Wouldn't the variant stuff still require me to have code like I mentioned above?

Except it would look like this:

class LuaInterpreter
{
public:
   void CallLuaFunction(const std::string& name);
   void CallLuaFunction(const std::string& name, const VariantType&);
   void CallLuaFunction(const std::string& name, const VariantType&, const VariantType&);
// etc

};

Hrmm, now that I think about it, I suppose I could do varargs, and just *know* that it's going to be a variant, do the cast, and therefore introducing the potential bug that you're mentioning.

Decisions, decisions. I think I like the templated goop better, except for it's lack of debuggabilit once it comes out of somethign like boost::preprocessor. I suppose I'll try both and hope for the best.

Thanks for the insight.

#8 .oisyn

    DevMaster Staff

  • Moderators
  • 1842 posts

Posted 23 March 2006 - 04:20 PM

If you want to avoid the template code in your LuaInterpreter class you could always make a function that stores all the passed arguments and their type information in an object, and use that object to extract the arguments.

So something like:
class LuaInterpreter
{
    void CallLuaFunction(const std::string & name, const std::vector<boost::any> & args);
};

template<class P1>
std::vector<boost::any> arguments(const P1 & p1)
{
    std::vector<boost::any> v;
    v.push_back(p1);
    return v;
}

template<class P1, class P2>
std::vector<boost::any> arguments(const P1 & p1, const P2 & p2)
{
    std::vector<boost::any> v;
    v.push_back(p1);
    v.push_back(p2);
    return v;
}

// etc


int main()
{
    LuaInterpreter li;
    li.CallLuaFunc("myFunc", arguments(1, 2, "hi there", 0.3435f));
}

But instead of boost::any you probably want your own type containing a reference to an interface that pushes an argument of a given type on the lua stack, so you aren't stuck with huge if-else-if structures to test the type in a boost::any to determine the appropriate actions to take.
C++ addict
-
Currently working on: the 3D engine for Tomb Raider.

#9 eddie

    Senior Member

  • Members
  • PipPipPipPip
  • 751 posts

Posted 23 March 2006 - 04:25 PM

Hrmm. That's not exactly what I want, but you gave me a *great* idea, .oisyn.

That reminded me of the named parameter idiom, and more specifically, boost's usage of operator % for boost::format.

I could potentially have something like this:



CallLuaFunction("foo").Param(param1).Param(param2).Param(param3) // etc, etc. 


It might not be what I asked for in the beginning, but it's an idea! I'd only need one templated function at that point, and I can deal with the conversions to boost::any internally.

INTERESTING. :D

Thanks!

#10 .oisyn

    DevMaster Staff

  • Moderators
  • 1842 posts

Posted 23 March 2006 - 04:58 PM

Or, you could actually overload the , operator to do that ;)

CallLuaFunction("foo"), param1, param2, param3;

C++ addict
-
Currently working on: the 3D engine for Tomb Raider.

#11 dave_

    Senior Member

  • Members
  • PipPipPipPip
  • 584 posts

Posted 23 March 2006 - 11:08 PM

Mattias Gustavsson said:

Personal preference :)
I've got to agree with .oisyn here. Templates are the only way to go. It will be stupidly in inefficient to use dynamic types when the compiler can calculate them at runtime.

#12 Mattias Gustavsson

    Senior Member

  • Members
  • PipPipPipPip
  • 413 posts

Posted 24 March 2006 - 10:11 AM

Depends on what you want to achieve. Performance is not always the number one goal.

Lots of things in programming are down to your specific circumstances and personal preference.

I prefer my way to having dozens of template functions to be able to cope with variable number of arguments. Others prefer the template method. Both have their advantages and disadvantages. All I'm saying is, make the tradeoffs that works for your project.

#13 dave_

    Senior Member

  • Members
  • PipPipPipPip
  • 584 posts

Posted 24 March 2006 - 03:21 PM

Mattias Gustavsson said:

Depends on what you want to achieve. Performance is not always the number one goal.

Lots of things in programming are down to your specific circumstances and personal preference.

I prefer my way to having dozens of template functions to be able to cope with variable number of arguments. Others prefer the template method. Both have their advantages and disadvantages. All I'm saying is, make the tradeoffs that works for your project.
true, but I prefer to use a different language, perhaps something with proper dynamic types.

#14 monjardin

    Senior Member

  • Members
  • PipPipPipPip
  • 1033 posts

Posted 24 March 2006 - 07:54 PM

Overloading the comma operator makes me uneasy. I'd prefer returning a functor.

CallLuaFunction("foo")(param1)(param2)(param3);


monjardin's JwN Meter (1,2,3,4,5,6):
|----|----|----|----|----|----|----|----|----|----|
*

#15 eddie

    Senior Member

  • Members
  • PipPipPipPip
  • 751 posts

Posted 27 April 2006 - 07:11 PM

I had some time to play with this idea at home, and I came up with a neat solution. It's pretty much as perfect as I can get it.

This is just example code, but it should compile and run for you (does for me under gcc 3.4.5):


#include <iostream>

using namespace std;


class MyFunction

{

public:

    MyFunction(const char* pFunctionName)

    {

        cout << "Received function " << pFunctionName << endl;

    }


    MyFunction& operator,(const char* pParam)

    {

        cout << "Received param " << pParam << endl;

        return *this;

    }

};


class MyFunctionCaller

{

public:

    void operator[](MyFunction)

    {

        cout << "Calling function!" << endl;

    };


};



int main(void)

{

    MyFunctionCaller myFunctionCaller;


    myFunctionCaller[ MyFunction("FunkName"), "one", "two", "three" ];


    return 0;

}



The output it gives looks like this:


Quote

Received function FunkName
Received param one
Received param two
Received param three
Calling function!

A couple of points:

  • I use the operator[] for the function caller, instead of operator()
  • You have to use a MyFunction("Foo") call first.

The first point is important, because if I were to use operator(), the compiler starts looking for an overloaded member that accepts more parameters than what I've got. operator[] only accepts one parameter, so it looks for an "operator," which will return a MyFunction.

The second point goes hand in hand for a couple of reasons. First, I'm basically chaining via operator, the instance of the MyFunction, and it needs to know what type that is. A simple char * doesn't automatically converted to a MyFunction (one of the times I was hoping implicit constructors would rear it's otherwise-ugly head) in this case.

Anyhow, I think it's pretty neat, and thought I'd share. It's very similar to the other posts up here, but gives a more familiar function-calling feel (... sort of?). :)

Anyways. Sorry for reposting to an old thread. :)

#16 Jare

    Valued Member

  • Members
  • PipPipPip
  • 247 posts

Posted 27 April 2006 - 09:00 PM

I love that last solution! Although the indexing is kinda ugly, you could always make it like this:
class MyFunctionCaller
{
public:
    MyFunctionCaller(MyFunction)
    {
        cout << "Calling function!" << endl;
    }
};

int main(void)
{
    MyFunctionCaller((MyFunction("FunkName"), "one", "two", "three" ));
    return 0;
}
The (( )) protocol is a common way to achieve variable number of parameters to macros, so the resulting syntax is not too weird.

#17 eddie

    Senior Member

  • Members
  • PipPipPipPip
  • 751 posts

Posted 27 April 2006 - 09:07 PM

See my first point. That won't work, at least it didn't, under gcc.

The reason I use operator[] versus operator(), is because operator[] forces you to only have one parameter. operator() allows multiple, which then gets confusing to the compiler -- "Are you passing me multiple parameters to this function? Or do you want me to use the comma operator?". I'm sure there's a rule in the C++ spec that says why the compiler doesn't try to lookup the comma operator in these cases, but I'll leave it to the all-knowing .oisyn to point that one out. :)

Anyhow, it works well enough. I'm pretty damned happy with it. :) Feel free to prove me wrong if you can send me code that compiles on gcc with operator() -- I couldn't.

#18 eddie

    Senior Member

  • Members
  • PipPipPipPip
  • 751 posts

Posted 27 April 2006 - 09:08 PM

Oh, I'm a tool. Now I see that you're using double parenthesis on the function call.

Yes, that would work, you're right. That said, between using [] or (()), I'd prefer using less brackets, and less wonky errors if you forget to use the double brackets.

Thanks for the kudos, by the by. ;)

#19 monjardin

    Senior Member

  • Members
  • PipPipPipPip
  • 1033 posts

Posted 27 April 2006 - 09:12 PM

You could drop the function caller class all together like this:
#include <iostream>
using namespace std;

class MyFunction
{
public:
    MyFunction(const char* pFunctionName)
    {
        cout << "Received function " << pFunctionName << endl;
    }
    ~MyFunction()
    {
        cout << "Calling function!" << endl;
    }

    MyFunction& operator,(const char* pParam)
    {
        cout << "Received param " << pParam << endl;
        return *this;
    }
};

int main(void)
{
    MyFunction("FunkName"), "one", "two", "three";

    return 0;
}

EDIT: Don't do this if you need to throw exceptions from the function!
monjardin's JwN Meter (1,2,3,4,5,6):
|----|----|----|----|----|----|----|----|----|----|
*

#20 eddie

    Senior Member

  • Members
  • PipPipPipPip
  • 751 posts

Posted 27 April 2006 - 09:36 PM

Well, a couple of reasons why I myself can't.

First, my "FunctionCaller" is my LuaInterpreter. This is a real object that actually holds state that is used in calling the function.

Second, I like having the [] (ideally, a single set of ()), surround my parameters. Makes it feel all lovely, like a regular function. :)

Third, my operator[] will, in the end, actually end up executing the function. As it stands in what you've got, it'll collect all the parameters and such, but not do anything with them. (Easily fixed, I know. Just sayin'. :))

But, good points if someone's looking for something like that.





1 user(s) are reading this topic

0 members, 1 guests, 0 anonymous users