Hi,
I would like to re-open a discussion about allowing lambdas inside unevaluated
contexts. For the record, here is a good explanation of the rationale for
disallowing them:
and the original discussion that started the debate:
Summary of the problem
Let me summarize my understanding of the rationale (please correct me if I'm
wrong on something):
1. Allowing lambdas in unevaluated contexts creates the problem that performing
SFINAE inside the lambda is hard to implement for compilers. For example:
template <typename X>
auto f(X x) -> decltype([](auto x) {
// Some expression(s) that might be invalid, but SFINAE should be
// performed. Since these expressions can be arbitrarily complex,
// this is like SFINAE on steroids, which is hard to implement.
}(x));
2. Knowing the type of a lambda is pretty much useless, since that type is
unique anyway. Hence, the following would hold:
static_assert(!std::is_same<
decltype([](auto x, auto y) { return x + y; }),
decltype([](auto x, auto y) { return x + y; })
>::value, "");
In particular, this means that code like
using Plus = decltype([](auto x, auto y) { return x + y; });
Plus plus = [](auto x, auto y) { return x + y; };
is not valid, which seems to reduce the usefulness of the feature.
3. Since lambdas have unique types, how should we mangle the signature of
a function having a lambda in it? Consider the following:
void f(decltype([](auto x) { }));
How should we mangle the signature of `f`, and how can we make sure that it
is mangled the same way inside each translation unit where this declaration
appears?
Having found a compelling use case for lambdas inside unevaluated contexts,
I think it is worth reconsidering that restriction, which seems too strong.
The use case
The use case is performing type-level computations (like Boost.MPL) using
generic lambdas rather than a custom domain specific language (like MPL
LambdaExpressions). This new way of doing type-level computations is exploited
in the Boost.Hana library [1]. Here is an explanation of that technique.
First, let's assume we have heterogeneous algorithms, which are basically
STL algorithms that work on tuples (a bit like Boost.Fusion, too):
// Returns the first element of `tuple` for which `pred` returns a
// true-valued `std::integral_constant`.
template <typename ...T, typename Predicate>
auto find_if(std::tuple<T...> tuple, Predicate pred);
It is also possible to define other algorithms like `for_each`, `transform`
and so on, but let's keep this minimal. Now, we can define a wrapper to
represent a type using an object, and we can define type traits (or arbitrary
metafunctions) as normal functions taking such wrappers:
template <typename T>
struct Type { using type = T; };
template <typename T>
std::is_pointer<T> is_pointer(Type<T>) { return {}; }
Finally, we can use these in conjunction with our heterogeneous algorithms
to perform type-level computations using the usual C++ syntax:
auto types = std::make_tuple(Type<int>{}, Type<float*>{}, Type<char>{});
auto first_ptr = find_if(types, [](auto t) {
return is_pointer(t);
});
using FirstPtr = decltype(first_ptr)::type;
Within this new paradigm, it would often be useful to have lambdas inside
unevaluated contexts in order to skip the intermediate `first_ptr` variable.
Indeed, it would be more natural to simply write
using FirstPtr = decltype(find_if(types, [](auto t) {
return is_pointer(t);
}))::type;
Lifting the restriction
Clearly, point (2) of the rationale given above does not apply anymore, since
there is a very clear use case of lambdas inside unevaluated contexts. While
you might not be convinced by the above example, Boost.Hana [1] contains many
serious examples where this would be useful.
Secondly, preventing lambdas from appearing in unevaluated contexts for the
sole purpose of point (3) is arguably way overkill. Indeed, we do not need
the actual type of a lambda to appear inside a function signature in the
general case; we merely need to be able to pass a lambda to another function
and then use `decltype` on the result. Similarly, it would be better (although
still too strong) if `decltype([]() {})` was prohibited for reason (3), but
prohibiting `decltype([](){} ())` (note the call to the lambda) seems to be
overly restrictive.
I have talked to Richard Smith about this at CppCon 2015, and IIRC he said
that restrictions on what could and couldn't appear inside a mangled name was
now being stated explicitly in the standard. Hence, point (3) might not even
be a valid reason anymore.
Finally, there's point (1) about SFINAE. IMO, it would be better than nothing
if the standard just said that no SFINAE could happen inside the lambda's body,
so that a hard error is triggered instead. This would lead the door open to
adding support for SFINAE inside lambdas at a later time, and would enable
many use cases right now.
Also, FWIW, Clang has a bug right now that _sometimes_ makes it possible to
use lambdas inside unevaluated contexts. So it is definitely possible to
implement it. See [2] for more information.
I'd like to gather thoughts and comments about lifting this restriction, and
to confirm that reason (3) does not apply anymore. If there are no reasons not
to proceed, I will then open a core issue (I was told it was the best way to
do this).
Regards,
Louis Dionne