Deev
Deev

Reputation: 481

Why are copy-capturing lambdas not default DefaultConstructible in c++20

C++20 introduces DefaultConstructible lambdas. However, cppreference.com states that this is only for stateless lambdas:

If no captures are specified, the closure type has a defaulted default constructor. Otherwise, it has no default constructor (this includes the case when there is a capture-default, even if it does not actually capture anything).

Why does this not extend to lambdas that capture things that are DefaultConstructible? For instance, why can [p{std::make_unique<int>(0)}](){ return p.get(); } not be DefaultConstructible, where the captured p would be nullptr?

Edit: For those asking why we would want this, the behavior only seems natural because one is forced to write something like this when calling standard algorithms that require functors to be default-constructible:

struct S{
  S() = default;
  int* operator()() const { return p.get(); }
  std::unique_ptr<int> p;
};

So, we can pass in S{std::make_unique<int>(0)}, which does the same thing.

It seems like it would be much better to be able to write [p{std::make_unique<int>(0)}](){ return p.get(); } versus creating a struct that does the same thing.

Upvotes: 8

Views: 226

Answers (1)

Nicol Bolas
Nicol Bolas

Reputation: 474016

There are two reasons not to do it: conceptual and safety.

Despite the desires of some C++ programmers, lambdas are not meant to be a short syntax for a struct with an overloaded operator(). That is what C++ lambdas are made of, but that's not what lambdas are.

Conceptually, a C++ lambda is supposed to be a C++ approximation of a lambda function. The capture functionality is not meant to be a way to write members of a struct; it's supposed to mimic the proper lexical scoping capabilities of lambdas. That's why they exist.

Creating such a lambda (initially, not by copy/move of an existing one) outside of the lexical scope that it was defined within is conceptually vacuous. It doesn't make sense to write a thing bound to a lexical scope, then create it outside of the scope it was built for.

That's also why you cannot access those members outside of the lambda. Because, even though they could be public members, they exist to implement proper lexical scoping. They're implementation details.

To construct a "lambda" that "captures variables" without actually capturing anything only makes sense from a meta-programming perspective. That is, it only makes sense when focusing on what lambdas happen to be made of, rather than what they are. A lambda is implemented as a C++ struct with captures as members, and the capture expressions don't even technically have to name local variables, so those members could theoretically be value initialized.

If you are unconvinced by the conceptual argument, let's talk safety. What you want to do is declare that any lambda shall be default constructible if all of its captures are non-reference captures and are of default constructible types. This invites disaster. Why?

Because the writer of many such lambdas didn't ask for that. If a lambda captures a unique_ptr<T> by moving from a variable that points to an object, it is 100% valid (under the current rules) for the code inside that lambda to assume that the captured value points to an object. Default construction, while syntactically valid, is semantically nonsense in this case.

With a proper named type, a user can easily control if it is default constructible or not. And therefore, if it doesn't make sense to default construct a particular type, they can forbid it. With lambdas, there is no such syntax; you have to impose an answer on everyone. And the safest answer for capturing lambdas, the one that is guaranteed to never break code, is "no."

By contrast, default construction of captureless lambdas can never be incorrect. Such functions are "pure" (with respect to the contents of the functor, since the functor has no contents). This also matches with the above conceptual argument: a captureless lambda has no proper lexical scope and therefore spawning it anywhere, even outside of its original scope, is fine.

If you want the behavior of a named struct... just make a named struct. You don't even need to default the default constructor; you'll get one by default (if you declare no other constructors).

Upvotes: 5

Related Questions