Why Does C++11'S Lambda Require "Mutable" Keyword For Capture-By-Value, by Default

Why does C++11's lambda require mutable keyword for capture-by-value, by default?

It requires mutable because by default, a function object should produce the same result every time it's called. This is the difference between an object orientated function and a function using a global variable, effectively.

Why is 'mutable' a lambda function attribute, instead of being a capture type?

There is a mention about your suggestion in n2651:

The syntax for lambda expressions could be extended to allow declaring
whether the closure members should be declared mutable or not.

This approach could be confusing to programmers, as the mutability is not a
property of the closure object, but rather the variables stored in the
closure.

I don't know if this is the only reason, but it does seem like it was considered.
However, in Herb Sutter's proposal, he suggests getting rid of mutable and not making the capture copies implicitly const, so we might see changes again.

C++ Lambdas: Difference between mutable and capture-by-reference

What is happening

The first will only modify its own copy of x and leave the outside x unchanged.
The second will modify the outside x.

Add a print statement after trying each:

a();
std::cout << x << "----\n";
b();
std::cout << x << '\n';

This is expected to print:

6
5
----
6
6

Why

It may help to consider that lambda

[...] expressions provide a concise way to create simple function objects

(see [expr.prim.lambda] of the Standard)

They have

[...] a public inline function call operator [...]

which is declared as a const member function, but only

[...] if and only if the lambda expression’s parameter-declaration-clause is not followed by mutable

You can think of as if

    int x = 5;
auto a = [=]() mutable { ++x; std::cout << x << '\n'; };

==>

int x = 5;

class __lambda_a {
int x;
public:
__lambda_a () : x($lookup-one-outer$::x) {}
inline void operator() { ++x; std::cout << x << '\n'; }
} a;

and

    auto b = [&]()         { ++x; std::cout << x << '\n'; };

==>

int x = 5;

class __lambda_b {
int &x;
public:
__lambda_b() : x($lookup-one-outer$::x) {}
inline void operator() const { ++x; std::cout << x << '\n'; }
// ^^^^^
} b;

Q: But if it is a const function, why can I still change x?

A: You are only changing the outside x. The lambda's own x is a reference, and the operation ++x does not modify the reference, but the refered value.

This works because in C++, the constness of a pointer/reference does not change the constness of the pointee/referencee seen through it.

Why can we avoid specifying the type in a lambda capture?

From cppreference:

A capture with an initializer acts as if it declares and explicitly captures a variable declared with type auto, whose declarative region is the body of the lambda expression (that is, it is not in scope within its initializer), [...]

Lambdas used the opportunity of a syntax that was anyhow fresh and new to get some things right and allow a nice and terse syntax. For example lambdas operator() is const and you need to opt-out via mutable instead of the default non-const of member functions.

No auto in this place does not create any issues or ambiguities. The example from cppreference:

int x = 4;
auto y = [&r = x, x = x + 1]()->int
{
r += 2;
return x * x;
}(); // updates ::x to 6 and initializes y to 25.

From the lambda syntax it is clear that &r is a by reference capture initialized by x and x is a by value capture initialized by x + 1. The types can be deduced from the initializers. There would be no gain in requiring to add auto.



In my experience n could have been just declared inside the lambda body with auto or int as datatype. Isnt it?

Yes, but then it would need to be static. This produces the same output in your example:

std::generate(v.begin(), v.end(), [] () mutable { 
static int n = 0;
return n++; });

However, the capture can be considered cleaner than the function local static.

Why do these lambda captured values have different types?

The behavior of Clang, GCC and MSVC in C++20 mode is correct for all standard versions supporting lambdas. decltype(i) and decltype(ri) yield the type of the named variables. It is not rewritten to refer to the members of the closure object, as would be the case for decltype((ri)). (see e.g. [expr.prim.lambda.capture]/14 in C++17 draft N4659 handling this specifically)

Apparently MSVC's default behavior for standard modes before C++20 is non-conforming in how lambdas are handled. According to the documentation the flag /Zc:lambda must be given to handle lambdas standard-conforming. With this option MSVC produces the same result as in C++20 mode for C++17 and C++14 mode as well.

See for example this bug report which was closed as not-a-bug with instruction to use this flag. Also note that it doesn't seem to be included in /permissive-.



Related Topics



Leave a reply



Submit