Working with C++0X/C++11: Lambdas – part 2 – Scope Capture

Since a lambda is a local function defined with a scope (i.e. inside another function) it can have access to the same variables that are in that scope. This is where the capture-mode comes in. (Perversely, this prevents lambdas from working with templates, since templates have to be defined in a global scope.) The takeaway idea here is this;

Variables that are specified in the capture-clause are specified when the lambda is created. Think of these like values passed into the constructor of a functor.

Variables that are specified in the parameter-list are passed in as an argument – that is – when the lambda is called. Think of these as the parameters passed in with operator() of a functor. Thus you have the options of capturing state when the lambda is created (unique to lambdas) or when it is called (just like any other C++ function).

In addition to that, you have the option of passing parameters in by value or by reference, and either at declaration time (lambdas only) or invocation time (like any other C++ function). This is the power of lambdas, something that a lot of other languages don’t have – the ability for C++ lambdas to have effects on an outside scope.

This not only allows you to use lambdas as local functions, but also to create lambdas that encapsulate state. This is useful for creating closures – where a function accesses a variable outside it’s scope – something I’ll talk about in the next installment. For now, let’s cover the capture syntax and how it works.

Let’s go over some scope access samples

.
    // inside some function...
   int n = 5, m = 6; // we have some variables
   // let's define some lambdas
   []{ n; }; // error - no capture mode defined, can't access "n";
   [=]{ n; m; }; // OK - n & m are accessible by value
   [n](){ n; m; }; // error - n is accessible, m is not
   [=]{ n++; }; // error - n is const
   [&n]{ n++; }; // OK - n is passed by reference
   [=]() mutable { n++; }; // OK - parameters are mutable
.

Before we examine this in more detail, we first need to be able to execute the lambda. Remember a lambda is like a local function, so we have defined lambdas, but we need a way to invoke them whenever we want. You can invoke them in place by adding an parameter list after defining them, or you can get a function pointer to them and invoke them later on. Let’s do the latter;

.
    auto myLambda = []() throw() -> void {}(); 
.

So here we’re defining a pointer to our lambda function and we’re using the auto declaration since we don’t know or care what the exact declaration is. “myLambda” is the pointer to the function.

So let’s look at the last lambda statement from the previous sample in more detail and let’s give it a name, then we’ll call it a few times and see how the scoped variables behave.

.
    int n = 5, m=6
    auto myLambda = // capture it's address - we're getting a function pointer here
    // define the lambda
    [n,&m] // we capture n by value, m by reference
    () mutable  // no parameters are passed in & can modify non-ref variables locally
    {
	 n++; // post-increment
	 m++; // post-increment
    }; 

    // at this point n=5, m=6;
    myLambda(); // inside the lambda, both get incremented
    // at this point n=5, m=7;
    myLambda(); // inside the lambda, both get incremented
    // at this point n=5, m=8;
.

By default, the parameters passed in are const and can’t be modified in the lambda body. Thus the use of the mutable modifier. You can see how the capture-by-value & capture-by-reference syntax let us modify the local value or the scoped-one. While it’s nice to be able to create local function right where they are needed, I think the real power in lambdas lies in using them as closures, encapsulating (i.e. capturing and hiding) some bit of state for use later – using them this way is called a closure and will the the topic of my next C++0x blog.

This entry was posted in C++0X/C++11, Code, Uncategorized. Bookmark the permalink.