Click here to Skip to main content
15,882,152 members
Articles / Programming Languages / C++
Tip/Trick

C++/CLI: Storing Lambda in a Delegate

Rate me:
Please Sign up or sign in to vote.
4.76/5 (13 votes)
1 Dec 2017CPOL2 min read 21.4K   194   9   6
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)


Written By
Software Developer (Senior)
United States United States
Software engineer dedicated to constantly learning and improving with a focus on self-documenting code and informed design decisions. Interested in most everything. Currently diving down the rabbit-hole of functional programming and category theory.

Comments and Discussions

 
QuestionExcellent Pin
hans.sch8-Aug-23 23:08
hans.sch8-Aug-23 23:08 
BugFails to compile in VS 2022 17.5 Pin
chausner12-Mar-23 10:48
chausner12-Mar-23 10:48 
PraiseOutstanding Pin
koothkeeper3-Dec-17 11:36
professionalkoothkeeper3-Dec-17 11:36 
GeneralRe: Outstanding Pin
Jon McKee3-Dec-17 12:21
professionalJon McKee3-Dec-17 12:21 
QuestionMy vote of 5 Pin
Alexandre Bencz12-Feb-17 14:01
Alexandre Bencz12-Feb-17 14:01 
AnswerRe: My vote of 5 Pin
Jon McKee12-Feb-17 19:43
professionalJon McKee12-Feb-17 19:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.