There are two, somewhat related motivations for an abbreviated lambda syntax. The first is to address the problem of trying to pass in overload sets as function arguments [1]:
C++14 generic lambdas allow us a way to solve this problem by just wrapping the overloaded name in a lambda:template <class T> T twice(T x) { return x + x; } template <class I> void f(I first, I last) { transform(first, last, twice); // error }
transform(first, last, [](auto&& x) { return twice(std::forward<decltype(x)>(x)); });
But that isn't actually correct, although it's the "obvious" code that most people would produce. It's not SFINAE-friendly and it's not noexcept
-correct. Which could lead to avoidable errors:
You'd really have to write:struct Widget; bool test(int ); bool test(Widget ); void invoke(std::function<bool(int)> ); // #1 void invoke(std::function<bool(std::string)> ); // #2 // error: unresolved overloaded function type invoke(test); // still error: no known conversion from std::string to int or Widget invoke([](auto&& x) { return test(std::forward<decltype(x)>(x)); });
// OK: calls #1
invoke([](auto&& x) noexcept(noexcept(test(std::forward<decltype(x)>(x))))
-> decltype(test(std::forward<decltype(x)>(x)))
{
return test(std::forward<decltype(x)>(x));
});
And that's a lot to have to type. Which brings me to the second motivation: not having to write that. For simple lambdas, those lambdas whose entire body is return expr;
, the noisy stuff you have to write to get it correct where you need to use it just drowns out the signal of what it was you wanted your lambda to do in the first place. That is assuming that I succeed in writing the same code in all three places without accidentally introducing subtle differences.
Arguably the only important code in the above block is that which has been marked in blue. All I want to do is test(x)
, why so much boilerplate?
This paper proposes the creation of a new lambda introducer, =>
, which allows for a single expression in the body that will be its return. This will synthesize a SFINAE-friendly, noexcept
-correct lambda by doing the code triplication for you.
At its simplest:
shall be exactly equivalent to the lambda:[](auto&& x) => test(x)
But since[](auto&& x) noexcept(noexcept(test(x))) -> decltype(test(x)) { return test(x); }
auto&&
has become such a regular choice of argument for lambdas, that too can become optional. An omitted type would be assumed as auto&&
. That is:
[](x) => test(x) // equivalent to the above
One of the last sources of boilerplate is std::forward
. In a lot of generic code, the uses of forward
overwhelm all the rest of the code, to the point where many talks and examples just omit references entirely to save space. I'm occasionally tempted to introduce a macro (#define FWD(x) decltype(x)(x)
) which is just wrong. Unlike std::move
and std::ref
, which are used to do non-typical things and deserve to be visible markers for code readability, std::forward
is very typically used in the context of using forwarding references. It does not have as clear a need to be a signpost.
This paper would like to see a shorter way to forward arguments and proposes non-overloadable unary operator >>
, where >>expr
shall be equivalent to static_cast<decltype(expr)&&>(expr)
.
Putting it all together we get:
Abbreviated lambdas will not allow for default arguments.// old way transform(first, last, [](auto&& x) noexcept(noexcept(test(std::forward<decltype(x)>(x)))) -> decltype(test(std::forward<decltype(x)>(x))) { return test(std::forward<decltype(x)>(x)); }); // proposed new way transform(first, last, [](x) => twice(>>x));
Other examples of improved usage as compared to C++14 best practices.
Sorting in decreasing order: roughly comparable typing, but arguably clearer:
std::sort(begin(v), end(v), std::greater<>{}); // C++14 std::sort(begin(v), end(v), [](x,y) => x > y); // this proposal
Sorting in decreasing order by ID
std::sort(begin(v), end(v), [](auto&& x, auto&& y) { return x.id > y.id; }); // C++14 std::sort(begin(v), end(v), std::greater<>{}, &Object::id); // ranges with projections std::sort(begin(v), end(v), [](x,y) => x.id > y.id); // this proposal
Calling an overload where SFINAE matters and getting it wrong is a mess:
bool invoke(std::function<bool(int)> f); // #1 bool invoke(std::function<bool(std::string)> f); // #2 invoke([](auto x) { return x == 2; }); // error! (283 lines of diagnostic on gcc) invoke([](auto x) -> decltype(x == 2) { return x == 2; }); // OK C++14: calls #1 invoke([](x) => x == 2); // OK this proposal: calls #1
Binding an overloaded member function that takes arbitrarily many arguments to an instance, even without noexcept
-correctness:
// C++14 [&obj](auto&&... args) -> decltype(obj.func(std::forward<decltype(args)>(args)...) { return obj.func(std::forward<decltype(args)>(args)); }; // this proposal [&obj](... args) => obj.func(>>args...)
Chaining lots of functions together from range-v3: summing the squares under 1000:
// C++14 int sum = accumulate(ints(1) | transform([](int i){ return i*i; }) | take_while([](int i){ return i < 1000; })); // this proposal int sum = accumulate(ints(1) | transform([](i) => i*i) | take_while([](i) => i < 1000));
std::begin
:
This is the same idea - we're duplicating the body to get SFINAE-correctness. This could just as easily become:template <typename C> constexpr auto begin(C& cont) -> decltype(cont.begin()) { return cont.begin(); }
This becomes far more dramatic for examples like the overloads oftemplate <typename C> constexpr auto begin(C& cont) => cont.begin();
std::not_fn
, which has four overloads of operator()
that all look like this:
In order to get SFINAE- andtemplate <class... Args> auto operator()(Args&&... args) noexcept(noexcept(!std::invoke(f, std::forward<Args>(args)...))) -> decltype(!std::invoke(f, std::forward<Args>(args)...)) { return !std::invoke(f, std::forward<Args>(args)...); }
noexcept
-correctness, we need to triple the body, which makes the whole function basically illegible at best and error-prone at worst. With the abbreviated syntax, this function can be implemented correctly as:
template <class... Args> auto operator()(Args&&... args) => !std::invoke(f, >>args...);
=>
can appear in code in rare cases, such as in the context of passing a the address of the assignment operator as a template non-template parameter, as in X<Y::operator=>
. However, such usage is incredibly rare, so this proposal would have very limited effect on existing code. Thanks to Richard Smith for doing a search.
Unary operator>>
cannot appear in legal code today, so that is a pure language extension.
The usage of =>
(or the similar ->
) in the context of lambdas appears in many, many programming languages of all varieties. A non-exhaustive sampling: C#, D, Erlang, F#, Haskell, Java, JavaScript, ML, OCaml, Swift. The widespread use is strongly suggestive that the syntax is easy to read and quite useful.