Monday, February 3, 2014

Capture Quirk in C++14

Consider this code, which is legal and has the same behavior in both C++11 and C++14:
  const int ci = 42;
  auto f = [ci]() mutable { std::cout << ci << '\n'; };
  f();      // prints 42
This captures the local variable ci by value, thus putting a copy of it inside f. What's interesting is that because ci is const, the copy of it is const, too. That means that attempting to modify the copy of ci inside f won't compile:
  const int ci = 42;
  auto f = [ci]() mutable
           { std::cout << ++ci << '\n'; };      // error! can't modify
                                                // copy of ci that's in f
I wrote about this a couple of years ago in my article, Appearing and Disappearing consts in C++.

Yawn, old news. I know. But C++14 has generalized lambda captures, which means that another way to capture ci by value is this:
  const int ci = 42;
  auto f = [ci = ci]() mutable                  // note revised capture mode
           { std::cout << ++ci << '\n'; };      // okay! copy of ci in f is non-const!
The spec for such captures says that the type of the variable being initialized (the left-hand ci) is determined by the rules used by auto, and auto ignores the constness of initializing expressions when a non-reference, non-pointer is being initialized. So when ci is captured by value using this syntax, the copy inside f isn't const. (This assumes that the lambda is mutable, as it is here. For an explanation of why, I'll again refer you to Appearing and Disappearing consts in C++.) So with C++14, we have two ways to capture a local variable by value: non-generalized and generalized. Non-generalized, which is valid in C++11 and C++14, retains the constness of the variable being captured in the type of the copy. Generalized, which is valid in C++14 only, discards the constness of the variable being captured in the type of the copy. To top things off, we have two syntaxes for non-generalized capture: implicit and explicit. Hence the choices for capturing ci by value are:
  const int ci = 42;
  auto f1 = [=]() mutable                      // non-generalized implicit copy
            { std::cout << ++ci << '\n'; };    // error! copy of ci in f is const

  auto f2 = [ci]() mutable                     // non-generalized explicit copy
            { std::cout << ++ci << '\n'; };    // error! copy of ci in f is const

  auto f3 = [ci = ci]() mutable                // generalized copy
            { std::cout << ++ci << '\n'; };    // fine, copy of ci in f is non-const
Personally, I was never wild about the const retention in non-generalized captures, so I'm pleased that in C++14, there's a way to work around it.

Scott

PS - For the terminally curious, the behavior I'm referring to is specified in 5.1.2/15 for non-generalized captures and 5.1.2/11 for generalized captures (in N3797, the current draft for C++14).

5 comments:

Veit said...

So I see, your taking a break from keeping up with the current language development is going well ... ;-)

Anonymous said...

Is there a way to enforce constness of a captured variable in a mutable lambda? ;-)

Scott Meyers said...

@Anonymous: If you want the constness of a captured variable to propagate to the copy inside the closure, use a non-generalized capture.

Unknown said...

If the variable captured is const outside the lambda, then simply use a non-generalized capture as Scott suggests. If it is mutable then you can turn a lambda as in

int i = 0;
int j = 0;
auto lambda = [=]() mutable {
// ... code that uses i and j ... //
}

into something like

int i = 0;
int j = 0;
auto lambda = [=]() mutable {
return [&]( const auto & j ) {
// ... code that uses i and j ... //
}( j );
}

so you have ensured that j is left read-only. I would only suggest that, if it is really important that j be const. The compiler will probably be able to optimize the overhead away, but the code will be harder to read.

tkamin said...

The difference between value capture ([x]) that performs deduction for the lambda member by discarding the reference (std::remove_reference_t<decltype(x)>) and init capture ([x=x]) that uses auto deduction rules (std::decay_t<decltype(x)>) has major effects on behavior of the code that captures raw arrays.

Lets consider following functions:
auto fooValue()
{
int array[10]{};
return [array](int i) { return array[i]; }
}

auto fooInit()
{
int array[10]{};
return [array=array](int i) { return array[i]; }
}

In case of the fooValue function the type of the lambda member will be int[10] and copy of the entire array will be stored inside of it. As consequence the invocation in the form fooValue()(1) are valid.

For the fooInit function, as a consequence to array to pointer conversion, the lambda will have member of int* type and will store only pointer to the local array. This pointer will invalidated after fooInit return and as consequence the expression fooInit()(1) has undefined behavior (dangling reference).

It is worth noting that above problem is effectively eliminated by use of std::array instead of raw array.