Reconsidering lambdas in unevaluated contexts

163 views
Skip to first unread message

Louis Dionne

unread,
Sep 29, 2015, 3:31:01 PM9/29/15
to ISO C++ Standard - Future Proposals
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


Thiago Macieira

unread,
Sep 29, 2015, 4:48:37 PM9/29/15
to std-pr...@isocpp.org
On Tuesday 29 September 2015 12:31:01 Louis Dionne wrote:
> 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?

This one is easy. It's mangled as "f(decltype(lambda#1 of the declaration))".
In other words, a circular reference to itself. By ODR, we know that all
declarations have the same lambda declaration, so we don't need to mangle it:
we know it's the same.

Of course, the function above isn't callable, as per your point #2 before.

--
Thiago Macieira - thiago (AT) macieira.info - thiago (AT) kde.org
Software Architect - Intel Open Source Technology Center
PGP/GPG: 0x6EF45358; fingerprint:
E067 918B B660 DBD1 105C 966C 33F5 F005 6EF4 5358

Richard Smith

unread,
Sep 29, 2015, 10:07:41 PM9/29/15
to std-pr...@isocpp.org
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


--

---
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
Visit this group at http://groups.google.com/a/isocpp.org/group/std-proposals/.

Louis Dionne

unread,
Oct 2, 2015, 1:37:24 PM10/2/15
to ISO C++ Standard - Future Proposals
Great, so it really seems like the only "issue" left is to decide what to do with SFINAE
inside the body of a lambda. I'll go ahead and open a core issue, and what to do with
SFINAE can be dealt with then.

Regards,
Louis

Agustín K-ballo Bergé

unread,
Oct 2, 2015, 2:03:40 PM10/2/15
to std-pr...@isocpp.org
On 10/2/2015 2:37 PM, Louis Dionne wrote:
> Great, so it really seems like the only "issue" left is to decide what
> to do with SFINAE
> inside the body of a lambda. I'll go ahead and open a core issue, and
> what to do with
> SFINAE can be dealt with then.

I'm not sure I understand this concern, how does the unevaluated context
make things differently? I don't know if "immediate context" has a
normative definition, but the body of a lambda does not say immediate
context to me.

Consider the example you gave:

template <typename X>
auto f(X x) -> decltype([](auto x) { /*...*/ }(x));
f(42);

I would expect that to be equivalent to this:

template <typename X, typename F>
auto f(X x, F&& f) -> decltype(std::forward<F>()(x));
f(42, [](auto x) { /*...*/ });

That lambda has a placeholder for return type, so decltype must
instantiate the body to deduce the return type. Any errors within the
instantiation of the body are not in the immediate context, and that
results in a hard error today.

Now change it to have an explicit return type:

template <typename X, typename F>
auto f(X x, F&& f) -> decltype(std::forward<F>()(x));
f(42, [](auto x) -> void { /*...*/ });

No instantiation happens here, and the expression within decltype is
well-formed regardless of the body of the lambda.

Regards,
--
Agustín K-ballo Bergé.-
http://talesofcpp.fusionfenix.com

Louis Dionne

unread,
Oct 2, 2015, 2:43:41 PM10/2/15
to ISO C++ Standard - Future Proposals
I think your point of view is the correct one. I mentionned this issue because 
it was raised in the original discussion [1] about lambdas in unevaluated
contexts. In that discussion (from 2010), Daniel Krügler said:

    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) [...]

However, after reading your analysis, I think the behavior of a lambda's body 
inside a SFINAE context is clear and well defined, and should be as you say.
FWIW, this is the behavior that I would expect and be happy with.

Seems like the only thing left is to write a core issue!

Regards,
Louis

Ville Voutilainen

unread,
Oct 3, 2015, 3:13:47 AM10/3/15
to ISO C++ Standard - Future Proposals
On 2 October 2015 at 21:43, Louis Dionne <ldio...@gmail.com> wrote:
> Seems like the only thing left is to write a core issue!

I think you need to write a paper and present it to Evolution. Something like
this is well beyond the limits of what we consider soluble by a Core issue.
Reply all
Reply to author
Forward
0 new messages