Click here to Skip to main content
15,861,168 members
Articles / Programming Languages / C++

Lightweight Generic C++ Callbacks (or, Yet Another Delegate Implementation)

Rate me:
Please Sign up or sign in to vote.
4.85/5 (54 votes)
15 Dec 2010CPOL9 min read 104.5K   2K   130   23
A fast, standards-compliant, and easy-to-use C++ callback implementation

Introduction

There has been much work done on the implementation of C++ delegates, evident by the fact that there are many questions and articles (easily found on sites like Stack Overflow and The Code Project) pertaining to them. Despite the effort, a particular Stack Overflow question[1] indicates that there is still an interest in them, even after all these years. Specifically, the asker of the said question sought a C++ delegate implementation that was fast, standards-compliant, and easy to use.

Delegates in C++ can be implemented through the use of function pointers. Don Clugston's article[2] discusses this topic in-depth, and the research that Clugston has done shows that member function pointers are not all the same sizes, making it difficult to create a delegate mechanism that could work with member function pointers directly. Nevertheless, Clugston provides a way to implement C++ delegates using an intimate knowledge of the most popular compilers' internal code generation scheme. While it works, it doesn't satisfy the standards-compliant requirement.

It is for this reason that the Boost Function library[3] internally uses the free store to store member function pointers. While this is the most obvious way to solve the problem, it adds significant runtime overhead, which doesn't satisfy the speed requirement.

Sergey Ryazanov's article[4] provides a solution that is standards-compliant and fast, using standard C++ templates. However, the syntax to instantiate a delegate in Ryazanov's implementation is messy and redundant, so it doesn't satisfy the ease-of-use requirement.

I presented a proof-of-concept delegate implementation that satisfies all three as an answer to the Stack Overflow question mentioned above. This article will discuss my answer in more detail.

Using the Code

The source code I provide includes support for global, static, and member functions, based on the delegate mechanism I will present later in this article. The following code snippet demonstrates this:

C++
using util::Callback; // Callback lives in the util namespace

class Foo
{
public:
    Foo() {}

    double MemberFunction(int a, int b)            { return a+b; }
    double ConstMemberFunction(int a, int b) const { return a-b; }
    static double StaticFunction(int a, int b)     { return a*b; }
};

double GlobalFunction(int a, int b) { return a/(double)b; }

double Invoke(int a, int b, Callback<double (int, int)> callback)
{
    if(callback) return callback(a, b);
    return 0;
}

int main()
{
    Foo f;

    Invoke(10, 20, BIND_MEM_CB(&Foo::MemberFunction, &f));      // Returns 30.0
    Invoke(10, 20, BIND_MEM_CB(&Foo::ConstMemberFunction, &f)); // Returns -10.0
    Invoke(10, 20, BIND_FREE_CB(&Foo::StaticFunction));         // Returns 200.0
    Invoke(10, 20, BIND_FREE_CB(&GlobalFunction));              // Returns 0.5
    
    return 0;
}

The macros BIND_MEM_CB and BIND_FREE_CB macros expand into expressions that return a Callback object bound to the function passed into it. For member functions, a pointer to an instance is passed into it as well. The resulting Callback object can be invoked upon as though it was a function pointer.

Note the use of the "preferred syntax" for specifying the function signature. For example, Callback<double (int, int)> is the type of a Callback object that can be bound to functions taking two int arguments and returning a double. This also means that invoking a Callback<double (int, int)> object requires two int arguments and returns a double.

Also note that the macro BIND_MEM_CB can accept a member function that is either const or non-const. If the passed function pointer points to a const member function, BIND_MEM_CB accepts only const T* instance pointers. Otherwise, it accepts T* with non-const member functions. The callback mechanism is therefore "const-correctness" aware. Both global and static functions are bound to callback objects via the BIND_FREE_CB macro. In either case, the provided library supports functions that accept 0 to 6 arguments.

Since the callback mechanism does not rely on the free store, it can be easily stored and copied around (given that their function signatures match). A Callback object can be treated like a boolean (via the safe bool idiom[5]) to test whether it is bound to a function or not, as demonstrated in the Invoke() function in the sample code above. Because of how the mechanism works, it is not possible to compare two Callback objects. Attempting to do so results in a compilation error.

A Callback object can be unbound (returned to the default state) by assigning an instance of NullCallback to the object:

C++
callbackObj = NullCallback();

Limitations and Compiler Portability

The library was designed with a "no-frills" approach. Callback objects do not keep track of object lifetimes - therefore, invoking a Callback object that is bound to an object that has been deleted or gone out of scope leads to undefined behavior. Unlike Boost.Function, the function signatures must match exactly - it is not possible to bind a function with a "close-but-not-exact" signature. The library is not intended to be a drop-in replacement for Boost.Function – it is meant to be a convenient lightweight alternative.

Although the code does not require C++0x features and uses nothing more than what I believe to be standard C++, the code does require a fairly capable (recent) compiler. The code will just not work on compilers such as Visual C++ 6.0.

That being said, the code has been successfully tested on Visual C++ 8.0 SP1 (version 14.00.50727.762), Visual C++ 9.0 SP1 (version 15.00.30729.01) and GCC C++ compiler version 4.5.0 (via MinGW). The code compiles cleanly with /W4 on Visual C++ and -Wall on GCC.

How It Works

The best way to understand the underlying mechanism is to start from a very primitive and naïve delegate implementation and work upwards from there. Consider a contrived implementation of callbacks:

C++
float Average(int n1, int n2)
{     
    return (n1 + n2) / 2.0f;
}

float Calculate(int n1, int n2, float (*callback)(int, int))
{
    return callback(n1, n2);
}

int main()
{
    float result = Calculate(50, 100, &Average);
    // result == 75.0f
    return 0;
}

This works well for pointers to global functions (and to static functions), but it doesn't work at all for pointers to member functions. Again, this is due to differing sizes of member function pointers, as shown by Clugston's article. Since "all problems in computer science can be solved by another level of indirection", one can create a wrapper function that is compatible with such a callback interface instead of passing member function pointers directly. Because member function pointers require an object to invoke upon, one should also modify the callback interface to accept a void* pointer to any object:

C++
class Foo
{
public:
    float Average(int n1, int n2)
    {     
        return (n1 + n2) / 2.0f;
    }
};

float FooAverageWrapper(void* o, int n1, int n2)
{     
    return static_cast<Foo*>(o)->Average(n1, n2);
}

float Calculate(int n1, int n2, float (*callback)(void*, int, int), void* object)
{
    return callback(object, n1, n2);
}

int main()
{
    Foo f;
    float result = Calculate(50, 100, &FooAverageWrapper, &f);
    // result == 75.0f
    return 0;
}

This "solution" works for any method in any class, but it is cubersome to write a wrapper function everytime it is needed, so it's a good idea to try to generalize and automate this solution. One can write the wrapper function as a template function. Also, since the member function pointer and an object pointer must come in pairs, one can stash both pointers into a dedicated object. Let's provide an operator()() so the object can be invoked just like a function pointer:

C++
template<typename R, typename P1, typename P2>
class Callback
{
public:
    typedef R (*FuncType)(void*, P1, P2);
    Callback() : func(0), obj(0) {}
    Callback(FuncType f, void* o) : func(f), obj(o) {}
    R operator()(P1 a1, P2 a2)
    {
        return (*func)(obj, a1, a2);
    }
    
private:
    FuncType func;
    void* obj;
};

template<typename R, class T, typename P1, typename P2, R (T::*Func)(P1, P2)>
R Wrapper(void* o, P1 a1, P2 a2)
{
    return (static_cast<T*>(o)->*Func)(a1, a2);
}

class Foo
{
public:
    float Average(int n1, int n2)
    {
        return (n1 + n2) / 2.0f;
    }
};

float Calculate(int n1, int n2, Callback<float, int, int> callback)
{
    return callback(n1, n2);
}

int main()
{
    Foo f;
    Callback<float, int, int> cb         
        (&Wrapper<float, Foo, int, int, &Foo::Average>, &f);
    float result = Calculate(50, 100, cb);
    // result == 75.0f
    return 0;
}

The wrapper function has been generalized by making it accept a function pointer via a feature of C++ templates called non-type template parameters. When the wrapper function is instantiated (by taking its address), the compiler is able to generate code that directly calls the function pointed by the template parameter in the wrapper function at compile-time. Since the wrapper function is a global function in this code, it can be easily stored in the Callback object.

This is in fact the basis of Ryazanov's delegate implementation[4]. It should be clear now why Ryazanov's solution did not satisfy the ease-of-use requirement – the syntax needed to instantiate the wrapper function to create the Callback object is unnatural and redundant. Therefore, more work needs to be done.

It seems odd that the compiler can't simply figure out the types making up the function pointer from the function pointer itself. Alas, it's not allowed by the C++ standard[6]:

A template type argument cannot be deduced from the type of a non-type template-argument. [Example:
C++
template<class T, T i> void f(double a[10][i]);
int v[10][20];
f(v); // error: argument for template-parameter T cannot be deduced

end example]

Another method of deduction must be used. It is well known that template argument deduction can be performed for function calls, so let's explore the possibility of using a dummy function to deduce the types of a function pointer passed into it:

C++
template<typename R, class T, typename P1, typename P2>
void GetCallbackFactory(R (T::*Func)(P1, P2)) {}

The types R, T, P1, and P2 are available inside the function. To "bring it outside" the function, one can return a dummy object, with the deduced types "encoded" into the type of the dummy object itself:

C++
template<typename R, class T, typename P1, typename P2>
class MemberCallbackFactory
{
};

template<typename R, class T, typename P1, typename P2>
MemberCallbackFactory<R, T, P1, P2> GetCallbackFactory(R (T::*Func)(P1, P2))
{
    return MemberCallbackFactory<R, T, P1, P2>();
}

Since the dummy object "knows" about the deduced types, let's move the wrapper functions and the Callback object creation code into it:

C++
template<typename R, class T, typename P1, typename P2>
class MemberCallbackFactory
{
private:
    template<R (T::*Func)(P1, P2)>
    static R Wrapper(void* o, P1 a1, P2 a2)
    {
        return (static_cast<T*>(o)->*Func)(a1, a2);
    }

public:
    template<R (T::*Func)(P1, P2)>
    static Callback<R, P1, P2> Bind(T* o)
    {
        return Callback<R, P1, P2>(&MemberCallbackFactory::Wrapper<Func>, o);
    }
};

template<typename R, class T, typename P1, typename P2>
MemberCallbackFactory<R, T, P1, P2> GetCallbackFactory(R (T::*Func)(P1, P2))
{
    return MemberCallbackFactory<R, T, P1, P2>();
}

Then, one can call Bind<>()on the temporary returned from GetCallbackFactory():

C++
int main()
{
    Foo f;
    Callback<float, int, int> cb = 
	GetCallbackFactory(&Foo::Average).Bind<&Foo::Average>(&f);
}

Note that Bind<>() is in fact a static function. The C++ standard allows static functions to be called on instances[7]:

A static member s of class X may be referred to using the qualified-id expression X::s; it is not necessary to use the class member access syntax (5.2.5) to refer to a static member. A static member may be referred to using the class member access syntax, in which case the object-expression is evaluated. [Example:
C++
class process {
public:
        static void reschedule();
}
process& g();
void f()
{
        process::reschedule(); // OK: no object necessary
        g().reschedule();      // g() is called
}

end example]
....

When the compiler encounters the call to Bind<>() in the expression above, the compiler evaluates GetCallbackFactory(), which helps deduce the types making up the function pointer. Once the deduction is made, the appropriate Callback factory is returned, then a function pointer can be passed to Bind<>() without having to explicitly supply the individual types. A Callback object is generated from the call to Bind<>() as a result.

Finally, a simple macro is supplied to simplify the expression. Since the macro expands into actual template function calls, the mechanism is still type-safe even though a macro is used.

C++
#define BIND_MEM_CB(memFuncPtr, instancePtr) 
	(GetCallbackFactory(memFuncPtr).Bind<memFuncPtr>(instancePtr))

int main()
{
    Foo f;
    float result = Calculate(50, 100, BIND_MEM_CB(&Foo::Average, &f));
    // result == 75.0f
    return 0;
}

This essentially completes the delegate mechanism. An inspection of the disassembly (from an optimized build) shows that the callback mechanism involves not much more than pointer assignments. Depending on the function bound to the callback object, the target function may be inlined into the wrapper function itself. But since there is an extra level of indirection, it's best to pass "big" objects by references. Otherwise, it should be fast enough for callbacks.

Conclusion

It is possible to implement an delegate system that is fast, compliant to the standard, and has a simple syntax. The C++ language has the facilities needed to achieve this goal, and with a capable enough compiler they can be used for the implementing C++ delegates. The solution presented in this article should be adequate for most applications.

History

  • Version 0.1 - November 29, 2010: Proof of concept
  • Version 1.0 - December 14, 2010: Initial release

References

  1. ^ 5 years later, is there something better than the “Fastest Possible C++ Delegates” ? Stack Overflow. 28 November 2010.
  2. ^ Don Clugston. Member Function Pointers and the Fastest Possible C++ Delegates. The Code Project. 23 May 2004.
  3. ^ Douglas Gregor. Chapter 7. Boost.Function. Boost. 25 July 2004.
  4. ^ a b Sergey Ryazanov. The Impossibly Fast C++ Delegates. The Code Project. 17 July 2005.
  5. ^ Bjorn Karlsson. The Safe Bool Idiom. Artima Developer. 31 July 2004.
  6. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §14.8.2.4 Deducing template arguments from a type [temp.deduct.type] para. 12.
  7. ^ ISO/IEC (2003). ISO/IEC 14882:2003(E): Programming Languages - C++ §9.4 Static members [class.static] para. 2.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
SuggestionSimilar implementation with lambdas support Pin
gildor225-Aug-18 9:22
gildor225-Aug-18 9:22 
QuestionLambdas Pin
nitro33129-Sep-13 1:15
nitro33129-Sep-13 1:15 
AnswerRe: Lambdas Pin
Member 128310663-Nov-16 6:40
Member 128310663-Nov-16 6:40 
QuestionGreat! Pin
dooDaddy30-Apr-13 13:21
dooDaddy30-Apr-13 13:21 
GeneralMy vote of 5 Pin
Chao Sun20-Jan-13 14:58
Chao Sun20-Jan-13 14:58 
GeneralMy vote of 5 Pin
WebMaster31-Dec-12 20:27
WebMaster31-Dec-12 20:27 
GeneralAn astonishing piece of work!! Pin
MikePelton21-Mar-12 1:12
MikePelton21-Mar-12 1:12 
QuestionHow can I check equality? Pin
JonKeon19-Oct-11 15:07
JonKeon19-Oct-11 15:07 
AnswerRe: How can I check equality? Pin
Elbert Mai19-Oct-11 19:09
Elbert Mai19-Oct-11 19:09 
GeneralRe: How can I check equality? Pin
JonKeon20-Oct-11 4:26
JonKeon20-Oct-11 4:26 
GeneralRe: How can I check equality? Pin
Izhaki12-Nov-12 13:07
Izhaki12-Nov-12 13:07 
QuestionExcelent Pin
Rafa_26-Jul-11 0:37
Rafa_26-Jul-11 0:37 
GeneralMy vote of 5 Pin
Paul Heil1-Mar-11 4:13
Paul Heil1-Mar-11 4:13 
GeneralMy vote of 5 Pin
Hans Dietrich4-Feb-11 9:51
mentorHans Dietrich4-Feb-11 9:51 
Generalstd::tr1::function Pin
Arman S.7-Jan-11 12:07
Arman S.7-Jan-11 12:07 
GeneralRe: std::tr1::function Pin
Elbert Mai12-Jan-11 17:34
Elbert Mai12-Jan-11 17:34 
GeneralMy vote of 5 Pin
smalti4-Jan-11 3:53
smalti4-Jan-11 3:53 
GeneralMy vote of 5 Pin
Faustino Frechilla20-Dec-10 3:10
Faustino Frechilla20-Dec-10 3:10 
GeneralMy vote of 5 Pin
Alain Rist18-Dec-10 5:32
Alain Rist18-Dec-10 5:32 
GeneralRe: My vote of 5 Pin
Elbert Mai18-Dec-10 9:02
Elbert Mai18-Dec-10 9:02 
GeneralMy vote of 5 Pin
Alexandre GRANVAUD15-Dec-10 20:30
Alexandre GRANVAUD15-Dec-10 20:30 
GeneralRe: My vote of 5 Pin
Elbert Mai18-Dec-10 9:16
Elbert Mai18-Dec-10 9:16 
GeneralMy vote of 5 Pin
djzbj15-Dec-10 16:04
djzbj15-Dec-10 16:04 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.