Introduction
I'm not a fan of re-inventing the wheel. Unfortunately, when I looked for solutions to storing a lambda in a delegate in C++/CLI, all the proposed solutions were both unnecessarily complex and didn't compile, frankly. So let's jump right into it! I included the header file above so you can just drop it in a project and test it out. Include guards are there in case your compiler doesn't recognize #pragma once
.
The Code
#include <utility>
namespace LambdaUtility
{
template< typename TLambda >
ref class LambdaWrapper
{
private:
TLambda* lambda_;
public:
LambdaWrapper(TLambda&& lambda): lambda_(new TLambda(lambda)) {}
~LambdaWrapper()
{
this->!LambdaWrapper();
}
!LambdaWrapper()
{
delete lambda_;
}
template< typename TReturn, typename... TArgs >
TReturn Call(TArgs... args)
{
return (*lambda_)(args...);
}
};
template< typename TDelegate, typename TLambda, typename TReturn, typename... TArgs >
TDelegate^ CreateDelegateHelper(
TLambda&& lambda,
TReturn(__thiscall TLambda::*)(TArgs...))
{
LambdaWrapper<TLambda>^ wrapper =
gcnew LambdaWrapper<TLambda>(std::forward<TLambda>(lambda));
return gcnew TDelegate(wrapper, &LambdaWrapper<TLambda>::Call<TReturn, TArgs...>);
}
template< typename TDelegate, typename TLambda, typename TReturn, typename... TArgs >
TDelegate^ CreateDelegateHelper(
TLambda&& lambda,
TReturn(__clrcall TLambda::*)(TArgs...))
{
LambdaWrapper<TLambda>^ wrapper =
gcnew LambdaWrapper<TLambda>(std::forward<TLambda>(lambda));
return gcnew TDelegate(wrapper, &LambdaWrapper<TLambda>::Call<TReturn, TArgs...>);
}
template< typename TDelegate, typename TLambda, typename TReturn, typename... TArgs >
TDelegate^ CreateDelegateHelper(
TLambda&& lambda,
TReturn(__thiscall TLambda::*)(TArgs...) const)
{
LambdaWrapper<TLambda>^ wrapper =
gcnew LambdaWrapper<TLambda>(std::forward<TLambda>(lambda));
return gcnew TDelegate(wrapper, &LambdaWrapper<TLambda>::Call<TReturn, TArgs...>);
}
template< typename TDelegate, typename TLambda, typename TReturn, typename... TArgs >
TDelegate^ CreateDelegateHelper(
TLambda&& lambda,
TReturn(__clrcall TLambda::*)(TArgs...) const)
{
LambdaWrapper<TLambda>^ wrapper =
gcnew LambdaWrapper<TLambda>(std::forward<TLambda>(lambda));
return gcnew TDelegate(wrapper, &LambdaWrapper<TLambda>::Call<TReturn, TArgs...>);
}
}
template< typename TDelegate, typename TLambda >
TDelegate^ CreateDelegate(TLambda&& lambda)
{
return LambdaUtility::CreateDelegateHelper<TDelegate>(
std::forward<TLambda>(lambda),
&TLambda::operator());
}
Not so bad, right? Using it is really simple as well. Example:
delegate String^ ConcatString(String^ s1);
char* s2 = " works!";
ConcatString^ test = CreateDelegate<ConcatString>([&](String^ s1) -> String^
{ return s1 + gcnew String(s2); });
Console::WriteLine(test("It"));
It works!
Points of Interest
&TLambda::operator()
TReturn(__clrcall TLambda::*)(TArgs...) const
TReturn(__thiscall TLambda::*)(TArgs...) const
This is where the magic happens. In order to support lambdas with returns and arguments (not just captures), I needed to figure out a way to determine the return and argument types. While their specific implementation isn't set in stone (i.e. don't rely on it), we know a couple things:
- They need to be able to store some kind of state for variable captures.
- This means they are some kind of class-like object internally (not just a function pointer).
- This object needs some way to execute.
- Since they need to execute, there must be an execution signature.
Well, the easiest way to allow execution would be implementing operator()
. Let's see if lambdas do that:
([]() -> void {Console::WriteLine("It works!"); })();
It works!
Bingo! So we pass &TLambda::operator()
into CreateDelegateHelper
. Now we can use a templated function pointer to grab the types. A regular function pointer won't work, however, as this is a pointer-to-member. This is why TLambda::*
is used. The last point that needs to be considered is that depending on the types involved the lambda signature can be either __thiscall
or __clrcall
. Putting everything together, we get TReturn(__clrcall TLambda::*)(TArgs...) const
and TReturn(__thiscall TLambda::*)(TArgs...) const
.
The only other "trick" is perfect forwarding through the templates of the lambda r-value by using std::forward
to avoid reference collapsing issues. This is an excellent article on rvalues, perfect forwarding, and forwarding references if you'd like to know more!
History
- 2/12/17: Initial release.
- 2/13/17: Updated download file and article code to support
mutable
lambdas and a namespace to de-pollute the global namespace with the helpers. Modified code format in article to prevent wrapping on long lines and so typename
is picked up by the code highlighter. - 11/27/17: Fixed incorrect history dates.
- 12/01/17: Erroneous update. Reverted to proper revision.