Crascit

Caffeine-powered software

Lambdas for lunch

Alright, so lambdas in C++ are cool and we’ve been coding with one arm tied behind our back all this time. C++11 brought us this wonderful goodness, which is great, but just how do you actually use them? No messing around, let’s jump right in!

A simple example

Consider the simple task of wanting to find the first negative value in a container. For the sake of demonstration, let’s assume you wanted to use the STL’s std::find_if algorithm, which requires a function or function-like object as one of its parameters to define the condition being searched for. A typical implementation might be:

std::vector<int> values = {23, -5, -2, 16, 20};

bool isNegative(int v)
{
    return v < 0;
}

auto pos = std::find_if(
               values.begin(),
               values.end(),
               isNegative);

Having to define the isNegative() operation as a separate function can be a bit of a nuisance, which is where lambdas can be useful. A lambda function allows the condition for the call to std::find_if to be written right there at the point of the call instead of having to define a separate, named function. This can be very convenient and allows code to be defined more locally to where it is used.

auto pos = std::find_if(
               values.begin(),
               values.end(),
               [](int v) { return v < 0; } );

This rather trivial example doesn’t really tap into some of the more powerful advantages of lambdas, but it does highlight how many people first encounter them. To really see how to exploit lambdas to good effect, we first need to understand their syntax.

Lambda syntax

The (slightly simplified) general form of a lambda function is:

[captures] (parameters) -> returnType {body}

The leading [] is what tells the compiler this is a lambda function. Unlike a regular function though, lambdas do not have a name, but if a lambda is assigned to a variable, then it can be treated just like a regular named function. Alternatively (and perhaps more commonly), lambdas can be defined directly as function arguments right at the point of the call, as in the simple example we showed earlier. The parameters defined with the lambda are the same as regular function parameters and the body is essentially just like an ordinary function’s body. The returnType is equivalent to an ordinary function’s return type, but when the compiler can deduce the return type (i.e. only one return statement in the body), it can be omitted. Putting aside the captures part of the syntax for a moment, some examples of lambdas help illustrate how they can be defined and used.

Compiler-deduced return type (double) with the lambda assigned to a variable. That variable is then used to call the lambda function:

auto squared = [](double t) { return t*t; };
std::cout << "5 squared = " << squared(5) << std::endl;

Lambda with multiple parameters and the return type explicitly defined:

auto addTrunc = [](double a, double b) -> int { return a+b; };
std::cout << addTrunc(4.3, 1.2) << std::endl;

Count the number of values divisible by 5, with the lambda defined as part of the call to count_if:

std::vector<int> values = {23, -5, -2, 16, 20};
auto c = std::count_if(values.begin(),
                       values.end(),
                       [](int i) { return i == ((i/5)*5); });
std::cout << "Result = " << c << std::endl;

Captures

The above examples are fairly trivial. The real power of lambdas comes from their ability to use variables and types from the enclosing scope in which the lambda is defined. The [captures] part of the lambda syntax is just a comma-separated list of the things that the body can refer to. These can be individual variables, the this pointer or a capture default.

Individual capture variables

The lambda can capture the value of a particular variable at the point the lambda is defined. This can be done in one of two ways:

  • Capture the variable’s value by copying it at the point where the lambda is defined. If the variable later changes value, the lambda still uses the original value. Copy captures are specified in the capture list simply using the variable’s name.
  • Capture the variable by reference, meaning that the variable’s value is retrieved when the lambda is executed, not when it is defined. Reference captures are specified by placing an ampersand (&) before the variable’s name in the capture list.

The following examples illustrate the difference:

int x = 5;

auto copyLambda = [x](){ return x; };
auto refLambda  = [&x](){ return x; };

std::cout << copyLambda() << std::endl;
std::cout << refLambda()  << std::endl;
x = 7;
std::cout << copyLambda() << std::endl;
std::cout << refLambda()  << std::endl;

The output from this block of code would be:

5
5
5
7

It is allowable to mix capture by copy and by reference for different variables, but you cannot capture a particular variable more than once:

auto lam1 = [x,&y](){ return x*y; }; // OK
auto lam2 = [&x,x](){ return x*x; }; // Error: x captured twice

C++14 extends the capabilities for individual variable captures, but we will not discuss those in this article.

Capturing the this pointer

When a lambda is defined inside a member function, it also has the ability to access the members of that class. The lambda is given that access by capturing the this pointer in a similar way to capturing a variable by copy:

class Foo
{
    int x;
public:
    Foo() : x(10) {}

    void bar()
    {
        // Increment x every time we are called
        auto lam = [this](){ return ++x; };
        std::cout << lam() << std::endl;
    }
};

Foo foo;
foo.bar(); // Outputs 11
foo.bar(); // Outputs 12

Capturing the this pointer is particularly convenient and lambdas often make use of this capability. Note that capturing this by reference doesn’t really make sense (you can’t change its value), so it should always appear in a capture statement as capturing by value.

Capture defaults

Capturing individual variables and the this pointer are very useful, but sometimes a default capture rule can be more convenient. Default captures prevent the need for having to explicitly list every variable referenced by the body in the lambda’s capture specification. A default capture is specified in one of two ways:

  • [=] means capture all variables by copying
  • [&] means capture all variables by reference

You can also mix default captures with individual variable captures, but the individual variable captures must be different to the default capture (i.e. both cannot by capture-by-copy or both be capture-by-reference). The individual captures act like overrides to the default capture. For example:

int x = 10;
int y = 14;
auto lam1 = []()       { return 24; };  // OK: capture nothing
auto lam2 = [=]()      { return x+y; }; // OK: copy x, copy y
auto lam3 = [&]()      { return x+y; }; // OK: reference x, reference y
auto lam4 = [=,&x]()   { return x+y; }; // OK: reference x, copy y
auto lam5 = [&,x]()    { return x+y; }; // OK: copy x, reference y
auto lam6 = [&x,=]()   { return x+y; }; // Error: default must be first
auto lam7 = [=,x]()    { return x+y; }; // Error: both specify copy x
auto lam8 = [=,this]() { return x+y; }; // Error: both specify copy this
auto lam9 = [&,&x]()   { return x+y; }; // Error: both specify reference x

In practice, most lambdas typically have their capture specification being empty (i.e. capture nothing) or just a small number of specific captures. Some people advocate against the use of default capture specifications on the grounds that they may capture some variables unintentionally, whereas having only specific variables captured makes things explicitly clear.

Real-world example: Qt

People working with Qt will be familiar with its signal-slot mechanism (Boost also has an analogous feature with its signals2 module). With Qt5, it became possible to use lambdas for the slots rather than having to define separate functions. This is particularly useful, since slots for handling UI events are often trivial one-liners. The following cut down code shows such an example:

class MyUI : public QDialog
{
    QLabel*    m_label;
    QLineEdit* m_lineEdit;
public:
    MyUI();
};

MyUI::MyUI()
{
    m_label    = new QLabel;
    m_lineEdit = new QLineEdit;

    // ... other initialisation of UI, etc.

    // Whenever the line edit's text changes,
    // update the label's text to show the
    // uppercase version of that string
    connect(m_lineEdit, &QLineEdit::textChanged,
    [this](const QString& s) { m_label->setText(s.toUpper()); });
}

Credit to Olivier Goffart for his useful article over at Woboq for this and other Qt-related C++11 ideas and techniques.

Closing remarks

The examples and discussion above really just scratch the surface of what lambdas can be used for. The complete, full lambda syntax provides some additional features not discussed here, but the material presented above covers the majority of typical uses. Lambdas are particularly handy when working with the STL and with toolkits like Qt or Boost which support passing around functions or function objects. Lambdas start becoming less useful once their body section gets more than a few lines long, where a separate named function may make the code more readable. Lambdas can also be a more efficient way of implementing callbacks than std::function(), a more detailed discussion of which can be found in the article OnLeavingScope: The sequel.

Previous

CMake targets with detailed dependencies

Next

Practical uses for variadic templates

1 Comment

  1. Jim

    This is an informative and well-written post. Nice job, and thanks, Craig.

Leave a Reply

Powered by WordPress & Theme by Anders Norén

%d bloggers like this: