C++11 delegates (callbacks) for cocos

Hi,

when writing cocos2d-x based game we often need some kind of callback system. Cocos2d-x 2.x uses C-like callback with all those selectors and call_func preprocessor machinery. It was pretty hard for me to remember all those macros and also difficult to debug, because of bug-prone C casts, so I’ve decided to create both more powerful and user-friendly C++11 callbacks/delegates.

Here is the code: https://gist.github.com/kuhar/8068893
It works well with gcc 4.6 and Visual Studio 2012.

Take a look at an example code below:

Delegate myDelegate; //create a delegate for functions/methods taking a pair of int and float, returning an int
myDelegate.add( this, &ThisClass::method ); // add a callback for some method
myDelegate.add( pOtherObject, &ThisClass::method );// add a callback to another object
myDelegate.add( []( int x, float y ){ LOG( "%d %f", x, y ); return 0; } );// add a lambda for logging calls

vector results = myDelegate.call( 4, 5.7f ); // call them all and get a vector of returned values

myDelegate.removeCallbacksForObject( this ); // do not call this
myDelegate.removeCallbacksForObject( pOtherObject );
int result = myDelegate.callOnlyOne( 3, 6.78f ); // call the only one (lambda logger in this case)

But it thing that the typical use will be just like this:
@ pSomeObject.setCallback( Delegate<void,CCNode*>( this, &SomeCalss::method ) ); //method takes a CCNode* and returns nothing (void)@

Delegates are designed to by passed by value/reference/move, because copying/swapping two small vector is quite fast, but you can obviously use pointer. Please note that they do not retain CCObjects passed to them, but check if they are nullptr before making a call.

What do you think of such solution?

rly great idea please add this to cocos :slight_smile:

I found the same, especially on actions. For example, adding a callback in a action sequence

You solution sounds interesting and will try it out :slight_smile:

Yeah you can create Action object that will contain your callback and then schedule it

Any one from cocos2d dev team?

So after spending some time with those delegates and many discussions with Dawid Drozd I’d come to following conclusions:
* they aren’t entirely type-safe. You can f.e. create a delegate with a pointer to CCObject and some method from CCNode. I’d cause an Undefined Behavior.
* they aren’t predictable: the order of adding functors and methods doesn’t change the calling order - methods are always invoked before functors.
* they are overly complicated: one usually doesn’t need to keep many methods and functors at once
* keeping methods and functors without a solution to manage their call priority is kinda useless

Hence, I’ve decided to simplify them and take care of correctness, rather than extending them. Now they just like the old-preprocessor-and-macro-based cocos callback, but they are extremely robust while being super-easy to use.

The code: https://gist.github.com/kuhar/8163865

Example of usage:

using namespace Utils;

Callback callback; // void --> return type, other types --> argument types
callback.set( pObject, &CCObject::methodWithInt );

/*or*/ Callback callback( pObject, &CCObject::methodWithInt );

/*or*/ Callback callback = makeCallback( pObject, &SomeClass::someMethod );

/*or even: */ auto callback = makeCallback( pObject, &SomeClass::someMethod );

/* then call methods with some args: */
int result = callback.call( some, args );

So, as you can see, you don’t even need to manually specify return type and arguments’ types, they can be automatically deduced from method’s signature by the compiler.

Now, it would be awesome to have some action like CCCallFunAction that would take a callback (and additional params). It’d be both simpler and powerful than old CCCallFunc.
I’m going to try creating a prototype tomorrow.

Could we create simple migration tool? For example on my old code i have old macros etc. could we wrapp those macros to use new callbacks?

Hi,

after some intensive use of my callbacks in our projects they’ve changed a bit. Now the template parameter follows std::function convention <retunType( Args… )> and there is an option to bind functions, static methods and non-capturing lambdas (essentially the same things).
The code is still available under the same link: https://gist.github.com/kuhar/8163865

I’ve done some research about callback in general and I’ve found out that current cocos 3.0 “callbacks” based on std::function and std::bind are tremendously slow when compared to my implementation.
The test was run on Windows machine with i7 processor with the following includes:
cocos2d.h (mocked): http://wklej.org/hash/d2e0603ac66/
Callback.h: https://gist.github.com/kuhar/8163865

And the test itself:

#include 
#include 
#include 
#include "Callback.h"

using namespace std;
using namespace cocos2d;
using namespace Utils;

void measureTime( void( *pFunction )( ) )
{
    auto start = std::chrono::steady_clock::now( );

    pFunction();

    auto end = std::chrono::steady_clock::now( );

    typedef std::chrono::duration millisecs_t;
    millisecs_t duration( std::chrono::duration_cast( end - start ) );
    std::cout << duration.count() << "\tms\n\n";
}

void testCallback()
{
    CCObject* pObject = new CCObject( );
    pObject->A = 3;
    CCWhatever* pWtw = new CCWhatever( );
    pWtw->A = 5;

    Callback first( pWtw, ( int ( CCWhatever::* ) ( int, float ) const )&CCWhatever::callMeIfYouCan );
    for( int i = 0; i < 10000000; ++i )
    {
        first.call(i, 3.0f );
    }

    for( int i = 0; i < 10000000; ++i )
    {
        first = Callback( []( int x, float b ) ->int { return x * ( int ) b; } );
        first.call( i, ( float ) i );
        first = makeCallback( pWtw, ( int ( CCWhatever::* ) ( int, float ) const )&CCWhatever::callMeIfYouCan );
        first.call( i, 3.0f );
        first = makeCallback( pWtw, &CCObject::callMeMaybe );
        first.call( i, 3.0f );
        first.getObject()->A = i;
    }

    delete pObject;
    delete pWtw;
}

void testAnyCallback( )
{
    CCObject* pObject = new CCObject( );
    pObject->A = 3;
    CCWhatever* pWtw = new CCWhatever( );
    pWtw->A = 5;

    function first = bind( ( int ( CCWhatever::* ) ( int, float ) const )&CCWhatever::callMeIfYouCan, pWtw, placeholders::_1, placeholders::_2 );
    for( int i = 0; i < 10000000; ++i )
    {
        first( i, 3.0f );
    }

    for( int i = 0; i < 10000000; ++i )
    {
        first = []( int x, float b ) ->int { return x * ( int ) b; };
        first( i, 3.0f );
        first = bind( ( int ( CCWhatever::* ) ( int, float ) const )&CCWhatever::callMeIfYouCan, pWtw, placeholders::_1, placeholders::_2 );
        first( i, 3.0f );
        first = bind( &CCObject::callMeMaybe, pWtw, placeholders::_1, placeholders::_2 );
        first( i, 3.0f );
        pWtw->A = i;
    }

    delete pObject;
    delete pWtw;
}

The results are:
# On VC 12: Callbacks: 105ms; std::function: 11714ms (112x slower)
# On VC 12.1: Callbacks: 105ms; std::function: 11706ms (111x slower)
# On GCC 4.8: Callbacks: 160ms; std::function: 3220ms (20x slower)

What do you think about these results in terms of general Cocos2d-x 3.0 performance?

Hi Jakub,

Thanks for sharing your code. I think in cocos2d-x v3.0 we are covering most of your ideas, since the API uses std::function instead of the old target/delegate pattern.

Regarding the performance tests, could you try the following ?

  • split the creation of the std::function from dispatching the callback.

I think that creating a std::function object is expensive because you are basicaly creating a new object. And when you use target/selector, you just basically pass two addresses.

Regarding dispatching, I think that both of them should have a similar performance, probably target/delegate could be slightly faster, but for sure, not 20x faster. Well, I could be wrong, but if that happens, the the compiler has a serious bug.

Thanks.

@Ricardo:

So, if I understood you correctly, after removing the second loop in each test I get 26ms and 75ms (2.88 x slower) on VC 12.1 and 9ms, 20ms on GCC 4.8 (2.22 x slower). All tests with O2.

But I think that creatnig/reassigning callbacks is pretty common when you use it as an alternative to CCNotifiactionCenter (and in similar cases) and it should not be undervalued. In such scenarios performance may be crucial. And the difference should be even greater when you’d store callback inside a vector, because of the std::function heap usage, thus harder to cache.

I’ve also done the tests on ARM (Samsung ATIV S, WP8 GDR3) (VC 11): Callbacks 432ms, std::function 902ms (2.09 x slower).

`Jakub:

thanks for the new data. Those numbers look more “right” to me.

In v3.0 we are using `std::function@ in the followings APIs:

  • MenuItem
  • CallFunc
  • Event Listener / Event Dispatcher
  • Texture Cache

And probably other parts.
In general, those APIs are “create once / dispatch multiple times”. So, yes, in v3.0 you pay a penalty for creating the callbacks, but you only pay a small penalty in the dispatching.

And what you gain is a more flexible API.

And you are correct about the heap. It is more expensive (memory-wise) to store an std::function than 2 longs. Although I don’t think this will be a big drawback.

@Ricardo:

And what you gain is a more flexible API.

The only gain that I can see is the ability to use capturing lambdas, which is not so useful in cocos IMHO. Edit: And they are not base-class dependent, but virtually all classes derive from CCObject, so it doesn’t matter.

>2 longs
Size of a member function pointer is implementation-defined, even up to 20B on VC.

Thanks.
Well, I do see a lot of benefits in using std::functions in the cocos2d API. But it is OK to disagree.

Size of a member function pointer is implementation-defined, even up to 20B on VC.

I didn’t know that. Thanks for the info.

@ricardo
could you give a simple example for std::functions ?

in my case, this is what I usually do:

Action *temp2 = Sequence::create(delay1, temp1, CallFunc::create(tempLeft, callfunc_selector(Sprite::removeFromParent)), NULL);

where temp1 is another Action*, delay1 is of type DelayTime* and tempLeft is of type Sprite*.

Can you tell me specifically how my code would change if I were to use std::functions api since CallFunc::create is declared deprecated.

@amynvirani

Do you mean something like this:

auto action1 = cocos2d::CallFunc::create(
        [&](){
                std::cout << "using a Lambda callback" << std::endl;
            });

or

std::function<void()> myFunction = []()
{
        std::cout << "From myFunction()" << std::endl;
};
auto action2 = cocos2d::CallFunc::create(myFunction); 
1 Like