This paper provides an alternative abbreviated lambda expression syntax, as well as an alternative syntax for transparent functions, compared to [@P0573R2].
[@P0573R2] introduces a notion of transparent functions, which aim to behave as close as possible to as if their body was directly inserted into the caller (except that their arguments are only computed a single time).
As described in the mentioned proposal, current function declarations and lambda-expressions have multiple problems with transparent functions:
- They take arguments and return by value by default (
auto), while we usually want to accept forwarding references and retain ref-qualifier for the return type (decltype(auto)). Copies should be explicit - They are not
noexcept-correct, i.e. they are not automaticallynoexceptif all the expressions within their body are non-throwing - They aren't SFINAE-friendly by default
There are additional problems not mentioned in [@P0573R2]:
- Function declarations are not
constexpr-correct, i.e. they aren't automaticallyconstexprif they satisfy the requirements for a constexpr function - Another problem is discussed in the following subsection
Lambda expressions are meant to reduce boilerplate, but today's lambdas, especially short ones, are cumbersome to use. Compare to other programming languages:
- A lambda returning 42:
- In Haskell:
\() -> 42(ignoring the nuances) - In Java:
() -> 42 - In Kotlin:
{ 42 } - In Swift:
{ 42 } - In C#:
() => 42 - In Rust:
|| 42 - In C++:
[] { return 42; }
- In Haskell:
- A lambda multiplying its argument by 2:
- In Haskell:
\x -> x * 2 - In Java:
x -> x * 2 - In Kotlin:
{ x -> x * 2 } - In Swift:
{ x in x * 2 } - In C#:
x => x * 2 - In Rust:
|x| x * 2 - In C++:
[](auto&& x) { return x * 2; }
- In Haskell:
- A lambda that adds its arguments:
- In Haskell:
\x y -> x + y - In Java:
(x, y) -> x + y - In Kotlin:
{ x, y -> x + y } - In Swift:
{ x, y in x + y } - In C#:
(x, y) => x + y - In Rust:
|x, y| x + y - In C++:
[](auto&& x, auto&& y) { return x + y; }
- In Haskell:
Other programming languages:
- Do not require an explicit capture clause, assuming capture by reference
- Do not require explicit parameter types, inferring them from context or assuming most general types
- Do not require an explicit
return, assuming it - Some languages have special short forms for zero and single-parameter lambdas
[@P0927R2] discusses how their "implicit lambdas" could be replaced with a lambda-based approach, but the syntax for a zero-parameter lambda expression would need to be as terse as possible.
The primary proposed syntax is |param1, param2, param3| expr, as in Rust.
Such a lambda:
- Assumes capture by reference
[&], except when the lambda is non-local, then assumes no capture[] - Assumes
auto&&declarators for all the parameters (attributes are allowed on the parameters) - Is equivalent to a normal lambda with a single
returnstatement, except when the return type isvoid, then it is equivalent to a normal lambda with an expression-statement - Avoids copies by deducing the return type using
decltype((expr)). Users will have to make copies explicitly where appropriate, that could be done usingautooperator proposed by [@P0849R2] - Is SFINAE-friendly
- Is
noexcept-correct: marked asnoexceptunless its expression is potentially-throwing
Capture customization is possible using |param1, param2, param3| [captures] expr syntax.
The syntax in this case is:
|params| [optional-captures] { statement… optional-expr }Such a lambda:
- Has all the traits of a single-expression abbreviated lambda, except that…
- Is not SFINAE-friendly
- Implicitly
returns the tailing (semicolon-less) expression, unless there is none such - Deduces the return type using the first
returnstatement or, if there is none such, the trailing expression
The syntax in this case is:
auto f(auto&& x, auto&& y) transparent { statement… optional-expr }Such a function:
- Is SFINAE-friendly iff the body only consists of the
expr - Is
noexcept-correct - Is
constexpr-correct - Implicitly
returns the trailing (semicolon-less) expression, unless there is none such - Avoids copies by deducing the return type using
decltype((expr-in-first-return)). Users will have to make copies explicitly where appropriate, that could be done usingautooperator proposed by [@P0849R2]
For the purposes of integration with SFINAE-friendliness and noexcept-correctness of [@P0573R2], we will only discuss single-expression lambdas.
[@P0927R2] and some other applications require that the lambda syntax is as brief as possible.
Any abbreviated lambda expression syntax must have a list of parameters (which may consist of zero or one parameter) and an expression, which is its body. It will therefore have a general form of:
… param1, param2, param3 … expr …
Because param1 would otherwise create an ambiguity (consider usage of lambda expression as a higher-order function argument), some separator is required before param1. For clarity when reading, some separator should be required before expr. A separator after expr is not required; expr is then defined to be an assignment-expression. The choice then boils down to choosing the appropriate "framing" of parameters. Several choices have been reviewed:
(x, y) f(x)is ambiguous with a C-style cast (single-parameter case) or with a comma-expression (multiple-parameter case)[x, y] f(x)is ambiguous with normal lambda expressions{x, y} f(x)is ambiguous with initializer-lists<x, y> f(x)visually conflicts with<…>usually meaning templates in C++|x, y| f(x)is potentially ambiguous with|and||operators, but not actually, because those are invalid where a lambda-expression can start