Why Are Lambda Expressions Not Allowed in an Unevaluated Operands But Allowed in the Unevaluated Portions of Constant Expressions

Why are lambda expressions not allowed in an unevaluated operands but allowed in the unevaluated portions of constant expressions?

The core reason for the unevaluated operands exclusion is covered in C++ Standard Core Language Defect Reports and Accepted Issues #1607. Lambdas in template parameters which seeks to clarify this restriction and states the intention of the restriction in section 5.1.2 was to:

[...] avert the need to deal with them in function template signatures [...]

As the issue documents the current wording actually has a hole since constant expressions allows them in an unevaluated context. But it does not outright state the rationale for this restriction. The desire to avoid name mangling stands out and you can infer that avoiding extending SFINAE was also desired since the proposed resolution seeks to tighten the restriction even though several viable alternatives would have allowed SFINAE. The modified version of 5.1.2 paragraph 2 as follows:

A lambda-expression shall not appear in an unevaluated operand (Clause 5 [expr]), in a template-argument, in an alias-declaration, in a typedef declaration, or in the declaration of a function or function template outside its function body and default arguments [Note: The intention is to prevent lambdas from appearing in a signature —end note]. [Note: A closure object behaves like a function object (20.10 [function.objects]). —end note]

This proposal was accepted and is in N3936(see this answer for a link)

For a more explicit discussion of the rationale to avoid having lambdas as an unevaluated operand. The discussion titled Rationale for lambda-expressions not being allowed in unevaluated contexts on comp.lang.cpp.moderated Daniel Krügler lays out three reasons:

  1. The extreme extension of possible SFINAE cases :

[...]The reason why they became excluded was due to exactly this extreme extension of sfinae cases (you were opening a Pandora box for the compiler)[...]


  1. In many cases it is just useless since each lambda has a unique type, the hypothetical example given:

    template<typename T, typename U>
    void g(T, U, decltype([](T x, T y) { return x + y; }) func);

    g(1, 2, [](int x, int y) { return x + y; });

    The type of the lambda in the declaration and the call are different(by definition) and therefore this can not work.

  2. Name mangling also becomes a problem since once you allow a lambda in a function signature the bodies of the lambda will have to be mangled as well. This means coming up with rules to mangle every possible statement, which would burdensome for at least some implementations.

C++: lambda-expression in unevaluated context

Your code is valid C++20 as written but invalid C++11.

  • Lambda expressions are not allowed in unevaluated contexts (such as decltype) before C++20.
  • Closure types are not default constructible before C++20. In C++20 a closure type that has no capture is default constructible.

Default template parameter & lambda in unevaluated context: bug or feature?

Could someone provide an explanation of the rules that make ok3 true
but ok4 false?

ok3 is true because uses lambdas type as default type.

The type of a lambda-expression (which is also the type of the closure
object) is a unique, unnamed non-union class type,

Hence, default template type for object, template parameter type for macro and singltone always different after every instatiation. But, for function function call returned lambda is unique and its type is unique. Template function ctad has template only for parameters but return value is unique. If rewrite function as:

template <class... Args, class T =  decltype([]{})>
ctad(const Args&...) -> ctad<T>;

In this case return type will be defferent after every instantiation.

Lambda expressions as class template parameters in C++14

No the situation in C++14 has not changed at all and in fact the language in section 5.1.2 Lambda expressions paragraph 2 has been tightened from:

A lambda-expression shall not appear in an unevaluated operand (Clause
5).

to:

[...]A lambda-expression shall not appear in an unevaluated operand
(Clause 5), in a templateargument, in an alias-declaration, in a
typedef declaration, or in the declaration of a function or function
template outside its function body and default arguments. [ Note: The
intention is to prevent lambdas from appearing in a signature. —end
note ][...]

Defect report 1607. Lambdas in template parameters lead to this change.

The defect report only obliquely deals with the rationale for disallowing this but we can find a very detailed explanation for why this is disallowed in Rationale for lambda-expressions not being allowed in unevaluated contexts. The reasons boil down to:

  • Lambda expressions not having a unique type
  • Compiler implementation issues:
    • Such as an extraordinary expansion of SFINAE
    • The possible requirement to name mangle the whole body of a lambda.

Given the rationale for this restriction it seems unlikely to change.

Unevaluated operands and auto return type

In order to determine the type denoted by decltype(h(42)), the compiler needs to perform template argument deduction for h and instantiate the template specialization h<int> to examine its body and determine the return type. This is not the same as evaluating h(42); for example, if h contained any side effects such as printing a message, those side effects would not occur.

Is there a special rule for lambda in case of decltype(auto)?

This answer is based on my interpretation of the relevant Standard text. Those sections are not very clear with divided opinions, and thus it is currently hard to know the exact meaning of them. It seems that, excluding a possible oversight, the major compilers seem to agree that the definition in question is indeed well-formed.

In addition, it is my opinion that it would be very surprising to hear that the definition were ill-formed.


Reason for the shock I've experienced lays in standard which says specifically that lambda should not occur in unevaluated operand [...]

Where do you see that a lambda appears in an unevaluated context?

decltype(auto) lambda = [](){};

I don't see it, because there is none. The lambda is used as an initializer, which is completely legal.

Now your confusion probably comes about because you seem to think that the above statement is equivalent to

decltype([](){}) lambda = [](){};

That's not the case though, strictly speaking. If you look at the language of the wording, there is a small difference (highlighted by me):

If the placeholder is the decltype(auto) type-specifierT shall be the placeholder alone. The type deduced for T is determined as described in [dcl.type.simple], as though e had been the operand of the decltype.

The key word here is though. It just means that the deduction happens as if it were decltype(e), meaning that the deduction rules of decltype apply instead of those for auto for the operand e.

Here, the operand e is indeed the lambda, but that is completely legal, because the Standard mandates that the behavior is the same as if you would have written decltype([](){}), meaning that of the rules of decltype deduction apply for the lambda. Now [expr.prim.lambda]/2 doesn't apply here, because the lambda is not in an unevaluated context, so it is actually legal for the compiler to use decltype([](){}) to deduce the type, meaning that the decltype rules have to be used for the lambda.

Sure, if you write decltype([](){}), the program is ill-formed, but that is not the case here, as mentioned above.

In this case, because a lambda expression is a prvalue, the deduced type should just be the type of the lambda.

At least that's how I understand it...

Evaluated constexpr lambda in non-type template argument

It is quite intentional that lambdas do not appear in unevaluated contexts. The fact that lambdas always have unique types leads to all sorts of issues.

Here are a few examples from a comp.lang.c++ discussion, from Daniel Krugler:

There would indeed exist a huge number of use-cases for allowing lambda
expressions, it would probably extremely extend possible sfinae cases
(to include complete code "sand-boxes"). The reason why they became
excluded was due to exactly this extreme extension of sfinae cases (you
were opening a Pandora box for the compiler) and the fact that it can
lead to problems on other examples as yours, e.g.

template<typename T, typename U>
void g(T, U, decltype([](T x, T y) { return x + y; }) func);

is useless, because every lambda expression generates a unique type, so
something like

g(1, 2, [](int x, int y) { return x + y; });

doesn't actually work, because the type of the lambda used in the
parameter is different from the type of the lambda in the call to g.

Finally it did also cause name-mangling issues. E.g. when you have

template<typename T>
void f(T, A<sizeof([](T x, T y) { return x + y; })> * = 0);

in one translation unit but

template<typename T>
void f(T, A<sizeof([](T x, T y) { return x - y; })> * = 0);

in another translation unit. Assume now that you instantiate f<int>
from both translation units. These two functions have different
signatures, so they must produce differently-mangled template
instantiations. The only way to keep them separate is to mangle the
body of the lambdas. That, in turn, means that compiler writers have
to come up with name mangling rules for every kind of statement in the
language. While technically possible, this was considered as both a
specification and an implementation burden.

That's a whole bundle of problems. Especially given that your motiviation of writing:

int N = S<[]()constexpr{return 42;}()>::value;

can be easily solved by instead writing:

constexpr auto f = []() constexpr { return 42; }
int N = S<f()>::value;

Passing lambda as a template parameter? ( c++20, lambdas in unevaluated context)

As mentioned in the comments, the problem is that your lambda captures u. Therefore, you the lambda cannot be instantiated from just its type in C++20. However, if you pass the lambda directly as a parameter, not its type, and you use an array instead of a vector (vector's constructor and operator[] are not constexpr), you can get it to work:

#include <array>
#include <iostream>
template <auto func>
struct Sequence
{
consteval auto operator[] (int i) const {
return func(i);
}
};
int main() {
constexpr auto u = std::array<int, 4>({1, 2, 3, 4});
constexpr auto l = [=](int i) { return u[i]; };
constexpr auto v = Sequence<l>{};
static_assert(v[1] == 2);
std::cout << v[1];
return 0;
}

Even without optimizations enabled, operator[] is never called at runtime.



Related Topics



Leave a reply



Submit