Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does capture by value in a C++ lambda expression require the value to be copied with the lambda object?

Tags:

c++

c++11

lambda

Does the standard define what happens with this code?

#include <iostream>

template <typename Func>
void callfunc(Func f)
{
   ::std::cout << "In callfunc.\n";
    f();
}

template <typename Func>
void callfuncref(Func &f)
{
   ::std::cout << "In callfuncref.\n";
    f();
}

int main()
{
    int n = 10;
    // n is captured by value, and the lambda expression is mutable so
    // modifications to n are allowed inside the lambda block.
    auto foo = [n]() mutable -> void {
       ::std::cout << "Before increment n == " << n << '\n';
       ++n;
       ::std::cout << "After increment n == " << n << '\n';
    };
    callfunc(foo);
    callfunc(foo);
    callfuncref(foo);
    callfunc(foo);
    return 0;
}

The output of this with g++ is:

$ ./a.out 
In callfunc.
Before increment n == 10
After increment n == 11
In callfunc.
Before increment n == 10
After increment n == 11
In callfuncref.
Before increment n == 10
After increment n == 11
In callfunc.
Before increment n == 11
After increment n == 12

Are all features of this output required by the standard?

In particular it appears that if a copy of the lambda object is made, all of the captured values are also copied. But if the lambda object is passed by reference none of the captured values are copied. And no copies are made of a captured value just before the function is called, so mutations to the captured value are otherwise preserved between calls.

like image 655
Omnifarious Avatar asked Dec 06 '22 15:12

Omnifarious


2 Answers

The type of the lambda is simply a class (n3290 §5.1.2/3), with an operator() which executes the body (/5), and an implicit copy constructor (/19), and capturing a variable by copy is equivalent to copy-initialize (/21) it to a non-static data member (/14) of this class, and each use of that variable is replaced by the corresponding data member (/17). After this transformation, the lambda expression becomes only an instance of this class, and the general rules of C++ follows.

That means, your code shall work in the same way as:

int main()
{
    int n = 10;

    class __Foo__           // §5.1.2/3
    {
        int __n__;          // §5.1.2/14
    public:
        void operator()()   // §5.1.2/5
        {
            std::cout << "Before increment n == " << __n__ << '\n';
            ++ __n__;       // §5.1.2/17
            std::cout << "After increment n == " << __n__ << '\n';
        }
        __Foo__() = delete;
        __Foo__(int n) : __n__(n) {}
      //__Foo__(const __Foo__&) = default;  // §5.1.2/19
    }
    foo {n};                // §5.1.2/21

    callfunc(foo);
    callfunc(foo);
    callfuncref(foo);
    callfunc(foo);
}

And it is obvious what callfuncref does here.

like image 73
kennytm Avatar answered May 20 '23 21:05

kennytm


I find it easiest to understand this behaviour by manually expanding the lambda to a struct/class, which would be more or less something like this (as n is captured by value, capture by reference would look a bit different):

class SomeTemp {
    mutable int n;
    public:
    SomeTemp(int n) : n(n) {}
    void operator()() const
    {
       ::std::cout << "Before increment n == " << n << '\n';
       ++n;
       ::std::cout << "After increment n == " << n << '\n';
    }
} foo(n);

Your functions callfunc and callfuncref operate more or less on objects of this type. Now let us examine the calls you do:

callfunc(foo);

Here you pass by value, so foo will be copied using the default copy constructor. The operations in callfunc will only affect the internal state of the copied value, no state changed in the actual foo object.

callfunc(foo);

Same stuff

callfuncref(foo);

Ah, here we pass foo by reference, so callfuncref (which calls operator()) will update the actual foo object, not a temporary copy. This will result in n of foo being updated to 11 afterwards, this is regular pass-by-reference behaviour. Therefore, when you call this again:

callfunc(foo);

You will again operate on a copy, but a copy of foo where n is set to 11. Which shows what you expect.

like image 35
KillianDS Avatar answered May 20 '23 19:05

KillianDS