C++17 In Detail

05 August 2020

C++ Lambda Week: Capturing Things

We’re in the second day of the lambda week. Today you’ll learn about the options you have when you want to capture things from the external scope. Local variables, global, static, variadic packs, this pointer… what’s possible and what’s not?

The Series

This blog post is a part of the series on lambdas:

The basic overview

The syntax for captures:

  • [&] - capture by reference all automatic storage duration variables declared in the reaching scope.
  • [=] - capture by value (create a copy) all automatic storage duration variables declared in the reaching scope.
  • [x, &y] - capture x by value and y by a reference explicitly.
  • [x = expr] - a capture with an initialiser (C++14)
  • [args...] - capture a template argument pack, all by value.
  • [&args...] - capture a template argument pack, all by reference.
  • [...capturedArgs = std::move(args)](){} - capture pack by move (C++20)

Some examples:

int x = 2, y = 3;

const auto l1 = []() { return 1; };          // No capture
const auto l2 = [=]() { return x; };         // All by value (copy)
const auto l3 = [&]() { return y; };         // All by ref
const auto l4 = [x]() { return x; };         // Only x by value (copy)
// const auto lx = [=x]() { return x; };     // wrong syntax, no need for
                                             // = to copy x explicitly
const auto l5 = [&y]() { return y; };        // Only y by ref
const auto l6 = [x, &y]() { return x * y; }; // x by value and y by ref
const auto l7 = [=, &x]() { return x + y; }; // All by value except x 
                                             // which is by ref
const auto l8 = [&, y]() { return x - y; };  // All by ref except y which 
                                             // is by value
const auto l9 = [this]() { }                 // capture this pointer
const auto la = [*this]() { }                // capture a copy of *this 
// since C++17

It’s also worth mentioning that it’s best to capture variables explicitly! That way the compiler can warn you about some misuses and potential errors.

Expansion into a Member Field

Conceptually, if you capture str as in the following sample:

std::string str {"Hello World"};
auto foo = [str]() { std::cout << str << '\n'; };
foo();

It corresponds to a member variable created in the closure type:

struct _unnamedLambda {
    _unnamedLambda(std::string s) : str(s) { } // copy

    void operator()() const {
        std::cout << str << '\n';
    }

    std::string str;  // << your captured variable
};

If you capture by reference [&str] then the generated member field will be a reference:

struct _unnamedLambda {
    _unnamedLambda(std::string& s) : str(s) { } // by ref!

    void operator()() const {
        std::cout << str << '\n';
        str = "hello"; // can modify values references by the ref...
    }

    std::string& str;  // << your captured reference
};

The mutable Keyword

By default, the operator() of the closure type is marked as const, and you cannot modify captured variables inside the body of the lambda.

If you want to change this behaviour, you need to add the mutable keyword after the parameter list. This syntax effectively removes the const from the call operator declaration in the closure type. If you have a simple lambda expression with a mutable:

int x = 1;
auto foo = [x]() mutable { ++x; };

It will be “expanded” into the following functor:

struct __lambda_x1 { 
    void operator()() { ++x; } 
    int x; 
};

On the other hand, if you capture things by a reference, you can modify the values that it refers to without adding mutable.

Capturing Globals and Statics

Only variables with automatic storage duration can be captured, which means that you cannot capture function statics or global program variables. GCC can even report the following warning if you attempt to do it:

int global = 42;

int main() {
    auto foo = [global]() mutable noexcept { ++global; };
    // ...
warning: capture of variable 'global' with non-automatic 
         storage duration

This warning will appear only if you explicitly capture a global variable, so if you use [=] the compiler won’t help you.

Capture with an Initialiser

Since C++14, you can create new member variables and initialise them in the capture clause. You can access those variables inside the lambda later. It’s called capture with an initialiser or another name for this feature is generalised lambda capture.

For example:

#include <iostream>

int main() {
    int x = 30;
    int y = 12;
    const auto foo = [z = x + y]() { std::cout << z << '\n'; };
    x = 0;
    y = 0;
    foo();
}

In the example above, the compiler generates a new member variable and initialises it with x+y. The type of the new variable is deduced in the same way as if you put auto in front of this variable. In our case:

auto z = x + y;

In summary, the lambda from the preceding example resolves into a following (simplified) functor:

struct _unnamedLambda {
    void operator()() const {
        std::cout << z << '\n';
    }

    int z;
} someInstance;

z will be directly initialised (with x+y) when the lambda expression is defined.

Captures with an initialiser can be helpful when you want to transfer objects like unique_ptr which can be only moved and not copied.

For example, in C++20, there’s one improvement that allows pack expansion in lambda init-capture.

template <typename ...Args> void call(Args&&... args) { 
    auto ret = [...capturedArgs = std::move(args)](){}; 
}

Before C++20, the code wouldn’t compile and to work around this issue, and you had to wrap arguments into a separate tuple.

Capturing *this

You can read more about this feature in a separate article on my blog:

Lambdas and Asynchronous Execution

Summary

This is just a short blog post that mentions the basic elements of the capture clause. But how about your experience? Do you have any tricks with capturing things? or non-standard use cases? Or what's your most useful capture "mode"? Let us know in comments below the article.

Next Time

In the next article, you’ll see how to go “generic” with lambdas. See here: Going Generic.

See More in Lambda Story

If you like to know more, you can see my book on Lambdas! Here are the options on how to get it and join 1000 of readers:

If you like my work and you want to get extra C++ content, exlusive articles and weekly curated news, then check out my Patreon website:

© 2017, Bartlomiej Filipek, Blogger platform
Disclaimer: Any opinions expressed herein are in no way representative of those of my employers. All data and information provided on this site is for informational purposes only. I try to write complete and accurate articles, but the web-site will not be liable for any errors, omissions, or delays in this information or any losses, injuries, or damages arising from its display or use.
This site contains ads or referral links, which provide me with a commission. Thank you for your understanding.