If you’ve ever tried to implement your own custom delegates, this amazing article must’ve crossed your path. If it didn’t, I can’t recommend enough that you read it’s contents entirely. It provides an extremely detailed and well structured information about member function pointers.
What the article seems to be missing is the support for lambda expressions. It’s understandable as Lambdas weren’t introduced into C++ when the article was written.
Therefore, in this article, I try to extend on the “Member Function Pointers and the Fastest Possible C++ Delegates” by Don Clugston and try to bind lambda to function pointer.
The basis of the implementation is to be able to bind/store a member function pointer as a variable of a generic member function pointer as follows:
class GenericClass{}; typedef void(GenericClass::*GenericMemberFuncType)(); //We want to be able to convert our lambda invocation to type "GenericMemberFuncType"
To achieve that, there must first be a way to bind invocation of lambda to a pointer of it’s own type. It’s pretty straight forward for Lambdas with no captured parameters and a little convoluted for Lambdas with capture parameters.
lambda with no Capture
It’s pretty straight forward to bind a lambda with no parameters to a function pointer. You can treat lambda as any other global or static function and assign it to a function pointer and invoke it.
auto noCapture =
[](int res) -> float
{
std::cout << "No capture lambda called with " << res << "\n";
return 99.9f;
};
typedef float(*NormalFuncType)(int);
NormalFuncType noCaptureLambdaPtr = noCapture; //----------- (1)
float res = noCaptureLambdaPtr(100); //----------- (2)
std::cout << "No capture lambda returned: " << res << "\n";
When we break at (1) and go into disassembler, there’s a curious “explicit conversion” happening under the hood. The next instruction then stores the contents of register eax into [noCaptureLambdaPtr]. If we go into this call by pressing F11 in disassembler, we see these sets of instructions.
It has regular stack push and pop instructions, but the instruction that’s highlighted is vital to have a lambda with no captures passable to a regular function pointer. It pushed a “lambda_invoker” into eax, which is eventually stored inside [noCaptureLambdaPtr]. So, calling this lambda_invoker should call out lambda.
Let’s try and make sense of this by emulating the same behavior in pure C++.
struct NoCaptureLambdaRep
{
static void Invoker() //....(@)
{
std::cout << "Called\n";
}
typedef void(*VoidType)();
operator VoidType() //.... (#) Explicit conversion to void(*)()
{
return &Invoker;
}
};
int main()
{
NoCaptureLambdaRep lambdaRep;
typedef void(*TestFunc)();
TestFunc testFunc = lambdaRep;
testFunc();
...
}
“NoCaptureLambdaRep” is analogous to the dynamic class generated by compiler for our lambda and “lambdaRep” is the lambda object that we are going to be working with.
In out first disassembly view, there’s an explicit conversion to a function pointer that’s allowing us to bind the returned value to a generic function pointer. (#) should emulate that behavior.
Next, the address of a Lambda_Invoker is stored inside eax, which is eventually passed to us. This is emulated as a static function at (@). The implementation logic of lambda would reside inside this function. Since it’s a static function, it can be safely converted to VoidType. Finally, calling “testFunc” will most definitely call NoCaptureLambdaRep::Invoker.
lambda with Capture
One thing is very pristine from the previous section. Lambdas are glorified objects generated by compilers. So, what happens when lambda has a capture list? Simple, they become members of our lambda class through shallow copy.
Since our Lambda now has member variables and we might be using them inside implementation logic, there’s would be no way to convert lambda’s invocation into a generic function pointer. Now more than ever, we must treat lambda the way it’s supposed to be. Like an object.
The only option in our arsenal to bind lambda to an external pointer is therefore, to bind to operator() (a member function) of type lambda to an external pointer of same type. This is how it could be implemented.
auto capture =
[p, q, r](int res) -> float
{
std::cout << "Capture lambda called with " << res << "\n";
std::cout << "Captured values: " << p << " " << q << " " << r << "\n";
return 101.1f;
};
typedef decltype(capture) CaptureLambdaType;
typedef float(CaptureLambdaType::*CaptureFuncType)(int) const;
CaptureFuncType captureLambda = &CaptureLambdaType::operator();
res = (capture.*captureLambda)(999);
std::cout << "Capture lambda returned: " << res << "\n";
When we break at (1) and look at disassembly, we see that it’s invoking the constructor of lambda and no clever type conversions take place and does exactly what we ask it to.
The same method of binding to a member function ptr of type lambda can also be used to bind lambdas with no captures.
This therefore, thins the line quite a lot between regular objects and lambdas. Therefore, with a few additional lines of code, we can easily extend Don Clugston’s article to include lambdas too.
One sneaky thing that crept up on me when implementing this was the realization that lambdas are designed to be short lived. So, when create lambda as a temporary inside function parameter, the original object could be destroyed. This makes all the original capture parameter data invalid.
auto del = QFastDelegate1<void, int>();
del.BindLambda(
[outerValue](int param)
{
std::cout << outerValue << " Called Delegate " << param << "\n";
}
);
We must therefore rely on heaps if we want to properly support lambda. Lambda is then copied to a structure maintained by our delegate. If delegate is copied, the data holding lambda should be deep copied. When delegate is destroyed, care must be taken to properly free the data if the callback stored is a Lambda!!
This is a slight overhead, but quite worth the trouble.
Well, that is all folks! Hope you got to learn something.