cplusplus / CWG

Core Working Group
24 stars 7 forks source link

[expr.prim.lambda.closure] Constraints on the function returned by a lambda conversion operator #351

Closed cor3ntin closed 1 year ago

cor3ntin commented 1 year ago

It is unclear if the function returned by the conversion operator of a lambda is constrained.

[expr.prim.lambda.closure]/p8 states

The closure type for a non-generic lambda-expression with no lambda-capture whose constraints (if any) are satisfied has a conversion function to pointer to function with C++ language linkage having the same parameter and return types as the closure type's function call operator. The conversion is to “pointer to noexcept function” if the function call operator has a non-throwing exception specification. If the function call operator is a static member function, then the value returned by this conversion function is the address of the function call operator. Otherwise, the value returned by this conversion function is the address of a function F that, when invoked, has the same effect as invoking the closure type's function call operator on a default-constructed instance of the closure type. F is a constexpr function if the function call operator is a constexpr function and is an immediate function if the function call operator is an immediate function.

Consider

template<class T> void f() {
  auto l = []() requires false { };
  l.operator(); //#1
  l(); // #2
}

Here the call operator has unsatisfied constraints. And while #1 is clearly ill-formed, is #2 also ill-formed? Indeed, if the call operator is not a valid candidate, then overload resolution will consider the conversion operator to function pointer in order to perform a surrogate call.

Presumably, #2 should be ill-formed, so we need a way to preserve the constraints.

Note that while investigating this, i did question whether calls to lambda expressions should consider their conversion operators but previous discussion suggest that it might be observable? https://lists.isocpp.org/ext/2021/12/18749.php

Suggested resolution

Otherwise, the value returned by this conversion function is the address of a function F that, when invoked, has the same effect as invoking the closure type's function call operator on a default-constructed instance of the closure type. F is a constexpr function if the function call operator is a constexpr function and is an immediate function if the function call operator is an immediate function. F has the same trailing requires-clause as the function call operator, if any.

jensmaurer commented 1 year ago

F has the same trailing requires-clause as the function call operator, if any.

I don't think that works. A trailing requires-clause is not part of a function type (see dcl.fct), instead it is part of the syntax of init-declarator. Thus, there is not really such a thing as a "pointer to function with a trailing requires-clause".

In short, the answer to

It is unclear if the function returned by the conversion operator of a lambda is constrained.

is: The conversion operator does not return a function, but a pointer-to-function, and such pointer types cannot syntactically be constrained.

I'm not seeing a specification defect leading to the claimed lack of clarity.

cor3ntin commented 1 year ago

I would be very surprised if the intent is for the example to be posted.

I think if we constrain F, it means that we cannot return it from the conversion operator, ie, taking its address would fail. But maybe constraining the conversion operator is more sensible then? I think your earlier reply suggested that.

After all, all of these should also probably be ill-formed

template<class T> void f() {
  auto l = []() requires false { };
  l.operator()(); //#1
  l(); // #2
  void(*ptr)() = l; // #3
}

template void f<int>();
jensmaurer commented 1 year ago

Maybe. The point is that constraints guide overload resolution (only), and don't mess with the function type. Constrained lambdas are useful for a std::overloads scenario, for example, and also for generic lambdas, I guess.

There is no evidence that the "constrained lambdas" feature was intended to affect the conversion to function pointer in any shape or form. (If I'm wrong, please point me to the relevant section in the corresponding paper once you've done the archeology.)

As such, I'm not seeing a CWG-level defect. If you believe the outcomes that you show are undesirable, feel free to suggest a change to EWG. (A trailing requires-clause can refer to the lambda's parameters, which are not in scope for the conversion operator, so I'm not seeing how this would work.)

There are a few options here:

This seems in need of an investigation and a design decision by EWG. I'm not going to create a CWG issue for a fishing expedition, if the existing wording is clear (although possibly the effects are undesirable).