Eichhörnchen Eichhörnchen - 10 months ago 93
C++ Question

Evaluated constexpr lambda in non-type template argument

Lambda expressions are not allowed in unevaluated contexts (e.g. in decltype) and could not be constant expressions until recently. Therefore there was no way to use them in template arguments.

In C++17 however constant expression lambdas will be possible. This still does not allow using them in template arguments in general.

However specifically for non-type template arguments constant expression lambda expressions could be used in an evaluated context, for example:

template<int N> struct S { constexpr static int value = N; };

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

That still doesn't work though, because lambda expressions are explicitly disallowed in template arguments whether type or non-type.

My question is the reasoning behind not allowing the construct above. I can understand that types of lambdas in function signatures might be problematic, but here the closure type itself is irrelevant, only the (compile-time constant) return value is used.

I suspect that the reason is that all statements in the lambda body would become part of the template argument expression and consequently SFINAE would need to be applied if any statement in the body is ill-formed during substitution. Probably that would require significant work from compiler developers.

But that is actually my motivation. If it was possible to use the construct above then SFINAE could be used not only with constant expressions, but also other statements valid in constexpr functions (e.g. literal type declarations).

Besides impact on compiler writers, is there any problem this would cause, e.g. ambiguities, contradictions or complications in the standard?

Answer Source

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;