Lightweight Generic C++ Callbacks (or, Yet Another Delegate 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:

 Collapse
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_CBmacro. 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.

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

 Collapse
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:

 Collapse
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:

 Collapse
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:

 Collapse
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:

 Collapse
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:

 Collapse
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:

 Collapse
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():

 Collapse
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]:

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.

 Collapse
#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.

转载于:https://my.oschina.net/zungyiu/blog/11660

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值