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.
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:
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 aCallback
object that can be bound to functions taking two int
arguments and returning a double
. This also means that invoking aCallback<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 aconst
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). ACallback
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 theInvoke()
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:
callbackObj = NullCallback();
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.
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:
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:
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:
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:
Collapse
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:
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:
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:
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()
:
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:
Collapse
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.
#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.
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.