Deducing Lambda Capture Types

Viewed 260

I've recently found that capturing a const object by value in a lambda, implies that the variable inside the labmda's body (i.e. the lambda's data member) is also const.
For example:

const int x = 0;
auto foo = [x]{
  // x is const int
};

This behavior is mentioned in § 8.1.5.2 in the draft for C++17:

For each entity captured by copy, an unnamed non-static data member is declared in the closure type. The declaration order of these members is unspecified. 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. A member of an anonymous union shall not be captured by copy.

I would expect that deducing type of captured variables will be the same as deducing auto.
Is there a good reason for having different type-deduction rules for captured types?

4 Answers

In your example, it would not be possible to modify x since the lambda is not mutable, which makes the function call operator const. But even if the lambda is mutable, it's true that the quoted passage makes the type of x in the lambda const int.

If I remember correctly, this was a deliberate design decision in C++11 to make the use of x within the lambda behave similarly to the use of x in the enclosing scope. That is,

void foo(int&);
void foo(const int&);
const int x = 0;
foo(x);  // calls foo(const int&)
auto foo = [x]() mutable {
    foo(x);  // also calls foo(const int&)
};

This helps to avoid bugs when, e.g., some code is rewritten from having an explicit loop to calling a standard library algorithm with a lambda.

If I'm wrong about this recollection, hopefully someone with the right answer will step in and write their own answer.

Not an answer to the reasoning; There is already a comprehensive answer here.

For those who want to know how to capture a non-const copy of a const variable, you can use a capture with an initialiser:

const int x = 0;
auto foo = [x = x]() mutable {
    // x is non-const
};

That requires C++14 though. A C++11 compatible solution is to make the copy outside the lambda:

const int x = 0;
int copy = x;
auto foo = [copy]() mutable {
    // copy is non-const
};

The reason is that operator() in lambda is const by default.

int main()
{
    const int x = 0;
    auto foo = [x](){}; // main::$_0::operator()() const
    foo();
}

So you have to use mutable lambda:

int main()
{
    const int x = 0;
    auto foo = [x=x](){}; // main::$_0::operator()()
    foo();
}

Is there a good reason for having different type-deduction rules for captured types?

This was deliberate decision (with rationale/reasons cited from CWG 756 below), and part of the rewrite of the original lambda proposal,

by

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 its resolution of CWG 756 [emphasis mine]:

[...] 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.

Related