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 automaticallynoexcept
if 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 automaticallyconstexpr
if 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
return
statement, 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 usingauto
operator proposed by [@P0849R2] - Is SFINAE-friendly
- Is
noexcept
-correct: marked asnoexcept
unless 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
return
s the tailing (semicolon-less) expression, unless there is none such - Deduces the return type using the first
return
statement 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
return
s 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 usingauto
operator 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