vinniefalco / LuaBridge

A lightweight, dependency-free library for binding Lua to C++
1.64k stars 346 forks source link

Replace upvalues with compile time instantiations #10

Open vinniefalco opened 12 years ago

vinniefalco commented 12 years ago

This is a continuation of the discussion from another issue:

Marton wrote: Nice. I am reading your code now. In my modifications to luabridge (internal unfortunately) I have had some of the same ideas as you did. For example, to key metatables using process-wide unique ids, only that mine are generated via

template <typename T>
inline void* class_id()
{
    struct x {};
    return (void*) typeid(x).hash_code();
};

But hash_code needs C++11. Your solution might be easier on memory, since typeid() generates a string internally, however, I don't know if it's not optimized away. Also, to get the ID of a const T, I simply use class_id(const T).

Also me I put the const methods additionally into the non-const metatable to save one lookup.

If I'm not mistaken, you also implemented the change I did too, to leave a class and its metatables on stack when registering its methods, to avoid looking it up every time.

Additionally to your changes, I got rid of all closures when pushing the proxies and to calculate the class uid inside the proxies on the fly. It might be faster (haven't benchmarked it though) and for sure it simplifies code.

vinniefalco commented 12 years ago

Marton wrote: You can get rid of the pointer to member by specifying it at compile time:

template <typename T, typename FnPtr, FnPtr F>
struct method_proxy;

template <typename T, typename Ret, Ret (T::*F)( /* params */ )>
struct method_proxy<T, Ret (T::*) ( /* params */ ), F>
{
    static int f(lua_State* L)
    {
        // ...
        T* obj = /*  the first param on stack */;
        Ret r = (obj->*F)( /* the params */);
        // ...
    }
};

Should be faster, too.

And then:

#define method(name, ptr) method__<decltype(ptr), (ptr)>(name)        

template <typename T>
template <typename FnPtr, FnPtr fp>
inline Class<T>& Class<T>::method__(const char* method_name)
{
    typedef method_proxy<T, FnPtr, fp> proxy;

    lua_pushcfunction(L, &proxy::f);

    // add const member functions only to const table
    if (std::is_const<typename proxy::class_type>::value)
    {
        lua_pushvalue(L, -1);
        add_overload(L, index + 1, method_name);
    }

    // add all member functions to non-const table
    add_overload(L, index + 2, method_name);

    return *this;
}
vinniefalco commented 12 years ago

Very nice. Compact, but dense. Is it possible to eliminate the method macro by putting another template in between method__ and proxy::f that has the <decltype (ptr), (ptr)> bit?

Marton: Can I just add a typedef ... Type to each existing specialization of FuncTraits in order to implement decltype? Then I could write typename FuncTraits <ptr>::Type as a replacement for decltype(ptr).

marton78 commented 12 years ago

I guess your typename FuncTraits <ptr>::Type solution should work, but you cannot get rid of specifying the type somehow: You have a typed template parameter F and its type must come from somewhere. -- see http://stackoverflow.com/questions/9275250/class-template-deduction-from-pointer-to-member

vinniefalco commented 12 years ago

Marton: Thinking this through, it seems that without the macro the only way to accomplish this is to move the function argument out of the parameter list and into the template argument list? So we would need:

getGlobalNamespace (L)
  .beginClass <A> ("A")
    .addMethod <&A::foo> ("foo");

or

getGlobalNamespace (L)
  .beginClass <A> ("A")
    .addMethod <A::foo> ("foo");

I think I'm okay with making this change, the syntax it not much different and eliminating the upvalue seems the right thing to do. It will take me some time though.

I added FuncTraits<...>::DeclType (in the develop branch), it's used like this:

typedef typename FuncTraits <MemFn>::DeclType MemFnType

MemFnType is now equivalent to decltype (MemFn).

marton78 commented 12 years ago

Of course it needs to go into the template parameters. See the #define in my code above. You'll need method__<typename FuncTraits <MemFn>::DeclType, MemFn>.

vinniefalco commented 12 years ago

I see now that the one-argument of addMethod() described above cannot be made to work..the closest you can get is addMethod <decltype <A::foo>, A::foo> ("foo") which is unfortunate

marton78 commented 12 years ago

Yes, it's awkward, hence the macro.

vinniefalco commented 12 years ago

I'm not terribly excited about using a macro. Is there no alternative?

vinniefalco commented 12 years ago

I'm thinking to provide both methods of registration, one with the function pointer passed as an argument and the other passed in the template parameter. If the user wants to create a macro that is possible. This way LuaBridge can offer the performance advantages if it is needed, or avoid the awkward syntax when the performance is not an issue.

vinniefalco commented 11 years ago

I'm working on LuaBridge again and reviewing the issues. Looking at this issue, is the performance gain really significant? One of the overriding design goals for LuaBridge is to be easy to use. Macros, and awkward syntax in the template arguments are definitely at odds with this goal.

marton78 commented 11 years ago

Well, I haven't measured and performance depends on the context, of course. But the advantage of this approach is that the resulting code (up to the invocation of lua_call) can be inlined, since everything is known to the compiler at compile time. The resulting code should be faster and smaller, but I guess it would be best to compare the generated assembly (with opimization turned on).

Another advantage is that this way all other proxies can forward to function_proxy, thereby decreasing code repetition. For example:

template <typename T, typename FnPtr, FnPtr F, typename Enable = void>
struct method_proxy;

template <typename T, typename Ret, typename Base, typename... Args, Ret (Base::*F)(Args...)>
struct method_proxy<T, Ret (Base::*) (Args...), F,
typename std::enable_if<std::is_base_of<Base, T>::value>::type>
{
    typedef T class_type;
    typedef Ret (*wrapper_type) (class_type*, Args...);

    static Ret wrapper(class_type* obj, Args... args)
    {
        return (obj->*F)(args...);
    }

    static int f(lua_State* L)
    {
        return function_proxy<wrapper_type, &method_proxy::wrapper>::f(L);
    }
};

// repeat the above for const member functions with: typedef const T class_type;

template <typename T, typename Ptr, Ptr p, typename Enable = void>
struct propget_proxy;

template <typename T, typename U, typename Base, U Base::*mp>
struct propget_proxy<T, U Base::*, mp,
typename std::enable_if<std::is_base_of<Base, T>::value>::type>
{
    typedef const T class_type;
    typedef U (*wrapper_type) (class_type*);

    static U wrapper(class_type* obj)
    {
        return obj->*mp;
    }

    static int f(lua_State* L)
    {
        return function_proxy<wrapper_type, &propget_proxy::wrapper>::f(L);
    }
};

template <typename T, typename U, typename Base, U (Base::*F)() const>
struct propget_proxy<T, U (Base::*) () const, F,
typename std::enable_if<std::is_base_of<Base, T>::value>::type>
: method_proxy<T, U (Base::*) () const, F>
{};

I wouldn't do such deep nesting if I weren't sure that my code is perfectly inlineable. About the quirky syntax: As long as I only see the macro, so that's no problem for me. But with overloaded member functions, where the type of the member function must be given explicitly, the macro can't be used, so that's ugly, yes:

lua::Class<Complex>(L, "Complex")
    .method__<void (Complex::*) (float), &Complex::set>("set")
    .method__<void (Complex::*) (const Complex&), &Complex::set>("set")

But in this case, even with the old-style proxies, you need to cast function pointers to the right type, so that's no difference. You could make a macro also for this, something like #define overloaded_method(Class, Return, Params, Name)

vinniefalco commented 11 years ago

I think that if we continue along these lines we will end up with an implementation identical to OOLua (http://code.google.com/p/oolua/). Which is beyond the scope of LuaBridge.