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

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

4.94/5 (73 votes)
4 Nov 2011CPOL7 min read 334.3K   2  
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.

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:

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

And here's the corresponding C++ version:

C++
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.

C#
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.

C++
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.

C++
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.

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

You get the following error.

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

The intellisense tooltip explains what happened there.

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

This code will compile.

C++
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.

C#
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.

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

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

C#
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):

C#
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.

C#
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.

C++
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).

C++
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):

C++
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:

ASM
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.

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

Now the syntactic equivalent C++ code follows.

C++
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.

C++
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.

C#
var f = x => x;

You'll get the following compiler errror.

C#
// 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.

C++
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).

C++
function<int(int)> copy = func;

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

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

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

ASM
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.

C#
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.

C#
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.

C++
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.

ASM
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.

MC++
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.

MC++
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.

MC++
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.

MC++
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)