Click here to Skip to main content
Click here to Skip to main content

Using lambdas - C++ vs. C# vs. C++/CX vs. C++/CLI

, 4 Nov 2011
Rate this:
Please Sign up or sign in to vote.
A comparative look at lambdas in C++ and C# with focus on the differences and similarities in lambda usage across the languages and their variants.
Prize winner in Competition "Best C++ article of November 2011"

Introduction

For a short while it was rather disappointing as a C++ developer that you did not have lambdas while other languages like C# already had them. Of course, that's changed now and C++ not only has lambdas but C++ lambdas have greater syntactic flexibility than C# lambdas. This article will be a comparative look at the lambda usage in both languages.

Note: The article does not intend to give a comprehensive syntactic coverage of lambdas in either C# or C++. It assumes you can already use lambdas in one or both languages. The article's focus is on the differences and similarities in lambda usage across the languages and their variants.

Basic usage in C# and C++

Here's a very simple C# method that I'll use to demo C# lambdas:

static void RunFoo(Func<int, int> foo)
{
    Console.WriteLine("Result = " + foo(3) + "\r\n");
}

And here's the corresponding C++ version:

void RunFoo(function<int(int)> foo)
{
    std::cout << "Result = " << foo(3) << "\r\n\r\n";
}

Both versions take a function that has an int parameter and returns an int.  Here's a very simple C# example showing how the method is called with a lambda.

RunFoo(x => x);

Notice how the argument type as well as the return type are inferred by the compiler. Now here's the C++ version.

RunFoo( [](int x) -> int { return x; });

The argument type and return type need to be specified in C++. Well, not exactly, the following will work too.

RunFoo( [](int x) { return x; });

Notice how I've removed the return type specification. But now, if I make a small change, it will not compile anymore.

RunFoo( [](int x)  { x++; return x; });

You get the following error.

// C3499: a lambda that has been specified to have a void return type cannot return a value	

The intellisense tooltip explains what happened there.

// the body of a value-returning lambda with no explicit return type must be a single return statement	

This code will compile.

RunFoo( [](int x) -> int  { x++; return x; });

Note that this may change in a future release where the return type will be deduced even for multi-statement lambdas. It is unlikely that C++ will support type-deduction in lambda arguments though.

Capturing variables - C# vs C++

Both C# and C++ let you capture variables. C# always captures variables by reference. The following code's output will make that fairly obvious.

var foos = new List<Func<int, int>>();

for (int i = 0; i < 2; i++)
{
    foos.Add(x =>
    {
        Console.WriteLine(i);
        return i;
    });
}
foos.ForEach(foo => RunFoo(foo));
foos.Clear();

That will output:

2
Result = 2

2
Result = 2

I reckon most of you know why that happens, but if you don't here's why. Consider this very basic code snippet.

int i = 5;
RunFoo(x => i);

The compiler generates a class (pseudo-code) that looks something like below:

sealed class LambdaClass
{
    public int i;
    public LambdaClass(){}
    public int Foo(int x){ return i;}
}

And the call to RunFoo is compiled as (pseudo-code):

var local = new LambdaClass();
local.i = 5;
RunFoo(new Func<int, int>(local.Foo));

So in the earlier example, the same instance of the compiler-generated class was reused inside the for-loop for each iteration. Which explains the output. The workaround in C# is to force it to create a new instance of the lambda class each time by introducing a local variable.

for (int i = 0; i < 2; i++)
{
    int local = i;

    foos.Add(x =>
    {
        Console.WriteLine(local);
        return local;
    });
}
foos.ForEach(foo => RunFoo(foo));

This forces the compiler to create separate instances of the lambda class (there's only one generated class, with multiple instances). Now take a look at similar C++ code.

std::vector<function<int(int)>> foos;

for (int i = 0; i < 2; i++)
{
  foos.push_back([i](int x) -> int
  {
    std::cout << i << std::endl;
    return i;
  });
}

for each(auto foo in foos)
{
  RunFoo(foo);
}
foos.clear();

In C++, you can specify how a capture is made, whether by copy or by reference. In the above snippet the capture is by copy and so the code works as expected. To get the same output as the original C# code, we could capture by reference (if that was desired for whatever reasons).

for (int i = 0; i < 2; i++)
{
  foos.push_back([&i](int x) -> int
  {
    std::cout << i << std::endl;
    return i;
  });
}

What happens in C++ is very similar to what happens in C#. Here's some pseudo-code that shows one plausible way the compiler might implement this (simplified version):

class <lambda0> 
{ 
  int _i; 
  
public: 
  <lambda0>(int i) : _i(i) {} 
  
  int operator()(const int arg) 
  {
    std::cout << i << std::endl;
    return i;
  } 
}; 

If you look at the disassembly, you will see a call to the () operator where the lambda is executed, something like the following:

00CA20CB  call `anonymous namespace'::<lambda0>::operator() (0CA1415h)  

The const-ness of captured variables

C++ captures variables as const by default, whereas C# does not. Consider the following C# code.

int val = 10;
RunFoo(x =>
{
    val = 25;
    return x;
});

Now the syntactic equivalent C++ code follows.

int val = 10;
RunFoo([val](int x) -> int
{
  // val = 25; <-- will not compile
  return x;
});

To lose the const-ness of the captured variable, you need to explicitly use the mutable specification.

RunFoo([val](int x) mutable -> int
{
  val = 25;  // compiles
  return x;
});

C# does not have a syntactic way to make captured variables const. You'd need to capture a const variable.

Local assignment

In C#, you cannot assign a lambda to a var variable. The following line of code won't compile.

var f = x => x;

You'll get the following compiler errror.

// Cannot assign lambda expression to an implicitly-typed local variable	

VB.NET apparently supports it (via Dim which is their var equivalent). So it's a little strange that C# decided not to do that. VB.NET generates an anonymous delegate and uses Object for all the arguments (since deduction is not possible at compile time).

Consider the C++ code below.

auto func =  [](int x) { return x; };

Here func is now of the compiler-generated lambda type. You can also use the function<> class (although in this case it's not needed).

function<int(int)> copy = func;

When you directly invoke the lambda, the code will be something like:

0102220D  call `anonymous namespace'::<lambda0>::operator() (1021456h)  

When you call it via the function<> object, the unoptimized code will look like:

0102222B  call  std::tr1::_Function_impl1<int,int>::operator() (102111Dh) 
 - - - >
        010284DD  call std::tr1::_Callable_obj<`anonymous namespace'::<lambda0>,0>::_ApplyX<int,int &> (10215FAh)  
         - - - >
                0102B73C  call `anonymous namespace'::<lambda0>::operator() (1021456h)  

Of course, the compiler will trivially optimize this so the release mode binary code will be identical in both cases.

Calling methods from lambdas

The following example shows a method being called from a C# lambda.

void Do(int x) { }

void CallDo() 
{
    RunFoo(x =>
        {
            Do(x);
            return x;
        });
}

What the C# compiler does here is to generate a private instance method that calls the method defined in the lambda.

private int <LambdaMethod>(int x)
{
    this.Do(x);
    return x;
}

It's this method that's passed to RunFoo as a delegate. Now imagine that you are also capturing a variable in addition to calling a member method. The compiler now generates a class that captures the variable as well as the this reference.

private class <Lambda>
{
    public int t;
    public Program __this;
    
    public <Lambda>() {}
    
    public int <CallDo>(int x)
    {
        this.__this.Do(x + this.t);
        return x;
    }
}

This is a little more obvious in C++ because you have to explicitly capture the this pointer to call a member function from the lambda. Consider the example below.

int Do(int h)
{
  return h * 2;
}

void Test()
{
  auto func =  [this](int x) -> int
  { 
    int r = Do(1);
    return x + r; 
  };

  func(10);
}

Notice how this has been captured there. Now when the compiler-generated lambda-class's () operator is called, invoking the method is merely a matter of calling that function and passing the captured this pointer.

call  T::Do (1021069h)

Lambdas in C++/CLI

One big disappointment for C++//CLI developers (all 7 of them) is the fact that C++/CLI does not support managed lambdas. You can use lambdas in C++/CLI but they will be native, and so you won't be able to easily interop with managed code that expects for instance  a Func<> argument. You'd have to write plumbing classes to convert your native lambdas to managed delegates. An example is shown below.

class LambdaRunner
{
  function<int(int)> _lambda;

public:
  LambdaRunner(function<int(int)> lambda) : _lambda(lambda)
  {
  } 

  int Run(int n)
  {
      return _lambda(n);
  }
};

The above class is the native implementation of the lambda runner. The following class is the managed wrapper around it.

ref class RefLambdaRunner
{
  LambdaRunner* pLambdaRunner;

  int Run(int n)
  {
    return pLambdaRunner->Run(n);
  }

public:
  RefLambdaRunner(function<int(int)> lambda)
  {
    pLambdaRunner = new LambdaRunner(lambda);
  }

  Func<int, int>^ ToDelegate()
  {
    return gcnew Func<int, int>(this, &RefLambdaRunner::Run);
  }

  void Close()
  {
    delete pLambdaRunner;
  }
};

Using it would look something like the following.

auto lambda = [](int x) -> int { return x * 2; };
auto lambdaRunner = gcnew RefLambdaRunner(lambda);
int result  = lambdaRunner->ToDelegate()(10);
lambdaRunner->Close();

Well that's a lot of work to get that running smoothly. With some clever use of macros and template meta programming, you can simplify the code that generates the native and managed runner-classes. But it'll still be a kludge. So a friendly advice to anyone planning on doing this is - don't. Save yourself the pain.

Lambdas with WinRT and C++/CX

You can use lambdas in C++/CX with WinRT types.

auto r  = ref new R();
r->Go();

auto lambda = [r]()
{
};

// or

auto lambda = [&r]()
{
}

The dev-preview may have a subtle bug or two, but the expected behavior is that when you capture by copy you will incur AddRef and Release, whereas when you capture by reference, you will not. The compiler will try and optimize this away for you in copy-capture scenarios when it thinks it's safe to do so. And this may be the source of one of the bugs in the dev preview where a Release is optimized away but the AddRef is not resulting in a potential memory leak.   But it's a certainty that this will all be fixed by beta, so I wouldn't worry too much about it.

Performance worries

Performance is always an obsession with C++ developers (and some C# and VB developers too). So very often you find people asking in the forums if using lambdas will slow down their code. Well without optimization, yes, it will. There will be repeated calls to the () operator. But any recent compiler will inline that, so you will not have any performance drop at all when you use lambdas. In C#, you will not have compile time optimizations but the CLR JIT compiler should be able to optimize away the extra indirections in most scenarios. A side effect is that your binary will be a tad bulkier with all the compiler generated classes, but it's a very small price to pay for the powerful syntactic and semantic value offered by lambdas, in either C++ or in C#.

Conclusion

Please do submit feedback and criticism via the article forum below. All feedback and criticism will be carefully read, and tragically sad moments of silence will be observed for the more obnoxious responses. *grin*

History

  • November 3, 2011 - Article first published.
  • November 4, 2011
    • Minor typo and formatting fixes
    • Added section on calling methods from lambdas

License

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

About the Author

Nish Sivakumar

United States United States
Nish is a real nice guy who has been writing code since 1990 when he first got his hands on an 8088 with 640 KB RAM. Originally from sunny Trivandrum in India, he has been living in various places over the past few years and often thinks it’s time he settled down somewhere.
 
Nish has been a Microsoft Visual C++ MVP since October, 2002 - awfully nice of Microsoft, he thinks. He maintains an MVP tips and tricks web site - www.voidnish.com where you can find a consolidated list of his articles, writings and ideas on VC++, MFC, .NET and C++/CLI. Oh, and you might want to check out his blog on C++/CLI, MFC, .NET and a lot of other stuff - blog.voidnish.com.
 
Nish loves reading Science Fiction, P G Wodehouse and Agatha Christie, and also fancies himself to be a decent writer of sorts. He has authored a romantic comedy Summer Love and Some more Cricket as well as a programming book – Extending MFC applications with the .NET Framework.
 
Nish's latest book C++/CLI in Action published by Manning Publications is now available for purchase. You can read more about the book on his blog.
 
Despite his wife's attempts to get him into cooking, his best effort so far has been a badly done omelette. Some day, he hopes to be a good cook, and to cook a tasty dinner for his wife.

Comments and Discussions

 
GeneralMy vote of 5 Pinmvpthatraja9-Jan-12 18:26 
GeneralRe: My vote of 5 PinmvpNishant Sivakumar28-Jan-12 11:06 

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

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

| Advertise | Privacy | Mobile
Web04 | 2.8.140721.1 | Last Updated 4 Nov 2011
Article Copyright 2011 by Nish Sivakumar
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid