Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

C++/CLI: Storing Lambda in a Delegate

4.76/5 (13 votes)
1 Dec 2017CPOL2 min read 22.3K   198  
How to do it and why it works

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

MC++
#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...);
        }
    };

    //Support for lambdas that use the mutable keyword
    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...>);
    }

    //Support for lambdas that are not mutable
    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:

MC++
delegate String^ ConcatString(String^ s1);

char* s2 = " works!";
ConcatString^ test = CreateDelegate<ConcatString>([&](String^ s1) -> String^ 
    { return s1 + gcnew String(s2); });
Console::WriteLine(test("It"));

//Output
It works!

Points of Interest

MC++
&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:

  1. They need to be able to store some kind of state for variable captures.
  2. This means they are some kind of class-like object internally (not just a function pointer).
  3. This object needs some way to execute.
  4. 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:

MC++
([]() -> void {Console::WriteLine("It works!"); })();

//Output
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.

License

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