Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why do fields in non-mutable lambdas use "const" when capturing const values or const references?

As seen in the question lambda capture by value mutable doesn't work with const &?, when capturing a value of type const T& using its name or [=] in a mutable lambda, the field in the hidden class gets type const T. It could be argued for that this is the correct thing to do for mutable lambdas.

But why is this done for non-mutable lambdas too? In non-mutable lambdas, the operator()(...) is declared const, so it can't modify captured values anyway.

The bad consequences of this happen when we move the lambda, for example when wrapping it in an std::function.

See the following two examples:

#include <cstdio>
#include <functional>

std::function<void()> f1, f2;

struct Test {
    Test() {puts("Construct");}
    Test(const Test& o) {puts("Copy");}
    Test(Test&& o) {puts("Move");}
    ~Test() {puts("Destruct");}
};

void set_f1(const Test& v) {
    f1 = [v] () {}; // field type in lambda object will be "const Test"
}

void set_f2(const Test& v) {
    f2 = [v = v] () {}; // field type in lambda object will be "Test"
}

int main() {
    Test t;
    puts("set_f1:");
    set_f1(t);
    puts("set_f2:");
    set_f2(t);
    puts("done");
}

We get the following compiler-generated lambda classes:

class set_f1_lambda {
    const Test v;
public:
    void operator()() const {}
};

class set_f2_lambda {
    Test v;
public:
    void operator()() const {}
};

The program prints the following (using gcc or clang):

Construct
set_f1:
Copy
Copy
Copy
Destruct
Destruct
set_f2:
Copy
Move
Move
Destruct
Destruct
done
Destruct
Destruct
Destruct

The v value is copied not less than three time in the first example set_f1.

In the second example set_f2, the only copy is when capturing the value (as expected). The fact that two moves are used is an implementation detail in libstdc++. The first move happens internally in operator= in std::function when passing the functor by value to an internal function (why doesn't this function signature use pass-by-reference?). The second move happens when move constructing the final heap-allocated functor.

The move constructor of the lambda functor object however, can't use a move constructor for a field if the field is const (since such a constructor can't "clear out" a const variable after stealing its content). That's why the copy constructor must be used instead for such fields.

So to me it seems to only have negative impact to capture values as const in non-mutable lambdas. Did I miss something important, or has it simply been standardised this way to make the standard more simpler somehow?

like image 749
Emil Avatar asked Aug 11 '21 00:08

Emil


People also ask

Are lambda captures Const?

By default, variables are captured by const value . This means when the lambda is created, the lambda captures a constant copy of the outer scope variable, which means that the lambda is not allowed to modify them.

What is capture clause in lambda function in c++?

A capture clause of lambda definition is used to specify which variables are captured and whether they are captured by reference or by value. An empty capture closure [ ], indicates that no variables are used by lambda which means it can only access variables that are local to it.

What is mutable in lambda?

The mutable keyword is used so that the body of the lambda expression can modify its copies of the external variables x and y , which the lambda expression captures by value. Because the lambda expression captures the original variables x and y by value, their values remain 1 after the lambda executes.


1 Answers

Did I miss something important, or has it simply been standardised this way to make the standard more simpler somehow?

The original lambda proposal,

differentiated between the captured object's type and the type of the corresponding data member of the lambda's closure type:

/6 The type of the closure object is a class with a unique name, call it F, considered to be defined at the point where the lambda expression occurs.

Each name N in the effective capture set is looked up in the context where the lambda expression appears to determine its object type; in the case of a reference, the object type is the type to which the reference refers. For each element in the effective capture set, F has a private non-static data member as follows:

  • if the element is this, the data member has some unique name, call it t, and is of the type of this ([class.this], 9.3.2);
  • if the element is of the form & N, the data member has the name N and type “reference to object type of N”; 5.19. CONSTANT EXPRESSIONS 3
  • otherwise, the element is of the form N, the data member has the name N and type “cv-unqualified object type of N”.

In this original wording OP's examples would not result in a const-qualified data member v. We may also note that we recognize the wording

in the case of a reference, the object type is the type to which the reference refers

which is present (but directly stating the type of the data member as opposed to the object type) in [expr.prim.lambda.capture]/10 of the (most recent draft of) the eventual wording on lambdas:

The type of such a data member is the referenced type if the entity is a reference to an object, an lvalue reference to the referenced function type if the entity is a reference to a function, or the type of the corresponding captured entity otherwise.

What happened was

  • N2927: New wording for C++0x Lambdas (rev. 2)

which re-wrote a large part of the wording from N2550:

During the meeting of March 2009 in Summit, a large number of issues relating to C++0x Lambdas were raised and reviewed by the core working group (CWG). After deciding on a clear direction for most of these issues, CWG concluded that it was preferable to rewrite the section on Lambdas to implement that direction. This paper presents this rewrite.

particularly, for the context of this question, resolving CWG issue

  • CWG 756. Dropping cv-qualification on members of closure objects

[...] Consider the following example:

void f() {
  int const N = 10;
  [=]() mutable { N = 30; }  // Okay: this->N has type int, not int const.
  N = 20;  // Error.
}

That is, the N that is a member of the closure object is not const, even though the captured variable is const. This seems strange, as capturing is basically a means of capturing the local environment in a way that avoids lifetime issues. More seriously, the change of type means that the results of decltype, overload resolution, and template argument deduction applied to a captured variable inside a lambda expression can be different from those in the scope containing the lambda expression, which could be a subtle source of bugs.

after which the wording (as of N2927) was made into the one we saw eventually go into C++11

The type of such a data member is the type of the corresponding captured entity if the entity is not a reference to an object, or the referenced type otherwise.

Were I dare to speculate, the resolution of CWG 756 also meant keeping cv-qualifiers for value-captures of entities that were of reference types, which may arguably have been an oversight.

like image 125
dfrib Avatar answered Oct 24 '22 04:10

dfrib