Alias expressions allow setting up a constructed object before returning it from a function or passing it as a function argument, while incurring no copies or moves.
Sometimes we want to create an object, set it up and return it.
widget setup_widget(int x) {
auto w = widget(x);
w.set_y(y);
return w;
}
setup_widget
will copy, or at least move w
out. Compilers often perform NRVO (Named Return Value Optimization) in such cases, but it's not guaranteed. We can't take a pointer to w
in setup_widget
and store it somewhere, because it will become dangling as soon as setup_widget
returns. And if widget
is non-copyable, the code is ill-formed.
Alias expression do (A -> B) { C }
:
- Computes expression
A
- Binds the result of
A
to the referenceB
- Executes the body
C
, which can use that reference - The result of the alias expression is the result of
A
No copies or moves involved!
Constructing, "cooking" and returning a non-movable widget
:
widget setup_widget(int x) {
int y = process(x);
// widget is non-copyable, non-movable
return do (widget(x) -> w) {
w.set_y(y);
};
}
Constructing and "cooking" a widget
in a function argument:
void consume_widget(widget w);
void set_widget(int x) {
int y = process(x);
consume_widget(do (widget(x) -> w) {
w.set_y(y);
});
}
The body of the do-expression can contain any statements, any usual control flow.
The expression in do(…)
is not required to be a constructor invocation, it can be any expression that returns a prvalue.
An alias expression is a primary expression of the form do (A -> B) C
where:
A
is a prvalue expressionB
is an identifierC
is a compound statement
Name B
belongs to the declarative region of the block scope of C
. In that block scope, B
is an lvalue of the same type as A
. The result object of B
is the result object of A
.
A
is sequenced-before C
. The alias expression is a prvalue whose result object is the result object of A
.
outside:
goto inside; // (1)
return do (widget() -> w) {
inside:
if (…) goto outside; // (2)
if (…) throw …; // (3)
if (…) return …; // (4)
};
If w
was a variable with automatic storage duration:
(1)
goto
from outside to inside would be ill-formed 👍(2)
On transfer out of the alias expression scope, the result object ofw
would be destroyed 👍(3)
During stack unwinding, the result object ofw
would be destroyed 👍(4)
w
would be destroyed after the expression in thereturn
statement executed 👎
But w
is not a variable with automatic storage duration. It also can't bind to a hidden variable, like structured bindings do.
The following wording is suggested to address those issues:
(1)
A program that jumps into an alias expression block scope from an outer scope is ill-formed.(2)
,(3)
On exit from the block scope of an alias expression by any means except reaching the end of the block (this includes stack unwinding), the result object ofw
is destroyed. The destruction is sequenced as if that object was the first object declared in the scope.(4)
return
statements and coroutineco_return
,co_await
,co_yield
operations are not allowed inside the block of an alias expression, including nested blocks.
We can, with cooperation from the returned object type.
Let's say widget
class from the examples defines the following constructor:
template <typename... Args, std::invocable<widget&> Func>
widget(Args&&... args, Func&& func)
: widget(std::forward<Args>(args)...)
{ std::invoke(std::forward<Func>(func)), *this); }
We can then use it to mimic an alias expression:
widget setup_widget(int x) {
int y = process(x);
return widget(x, [&](widget& w) {
w.set_y(y);
});
}
This approach satisfies all use cases of an alias expression. However, it requires cooperation from widget
and breaks when some of its other constructors accept an invocable parameter.
We cannot implement this functionality in general. Let's say we want to define an also
function (akin to Kotlin's "also" function) that takes an object, a lambda and applies the lambda to the object:
template <typename T, invocable Func>
T also(T x, Func func) {
func(x);
return x; // Houston!
}
There is no way to implement such a function, because we don't have guaranteed NRVO.
Currently, the only way we can obtain a reference to an object and then return this object from a function (without copies) is to have a special constructor in the object's type, as shown above.
Practically, no. Let's say we want the following function to have guaranteed NRVO:
widget setup_widget() {
auto w = widget(x);
*arbitrary-code*
return w;
}
We guarantee NRVO by saying that w
in return w;
is a prvalue (as an exception to the rule), and w
names the result of setup_widget
. (In other words: the result object of widget(x)
is the same as the result object of w
in return w
.)
However, this is not safe unless we know that *arbitrary-code*
can't lead to any return from the function except return w;
. This requires control flow analysis and is impossible in general.
We could require that NRVO is enforced in cases where the analysis is simple, e.g. "no return statements between those two". But what about goto
? What about other constrol flow? A lot of arbitrary restrictions are needed that aren't with alias expressions. WG21 has already made a decision that new features should not require such analysis.
The wording in "Strict definition" section carefully avoids that. Having such an lvalue conforms to existing value category rules, as well as using *this
in a constructor does (see "Can we implement similar functionality in C++ today?" section).
A lambda expression already contains statements "inside that expression". But strictly speaking, a lambda expression describes a completely different (member) function, and those statements are enclosed within that function.
So it is indeed a precedent that an expression contains statements. If we go down that path, we might as well allow "blocks" in expressions.
A similar statement-based feature could be proposed: aliasing return statement (not to be confused with an alias-declaration statement), but it would only work in the special case of returning a prvalue:
widget setup_widget(int x) {
int y = process(x);
return (widget(x) -> w) {
w.set_y(y);
};
}
do (widget() -> w) { use(w); }
as used throughout this proposal- ➕ Similar in structure to familiar
if
,while
,for
syntax
- ➕ Similar in structure to familiar
do (w = widget()) { use(w); }
- ➕ The familiar look of a variable initializer or a lambda capture initializer
- ➖ There is no actual construction or assignment happening in the parentheses
do (widget()) w { use(w); }
do widget() -> w { use(w); }
- ➕ Reduces the nesting of parentheses
- ➖ Inconsistent with the other C++ constructs
There may be other syntax options not using do
keyword.
This proposal is related to N4034 and P1144 in that they too can guarantee in more cases than today that moves won't occur. However, those instead suggest that relocation occurs (in N4034, relocation constructor will be called). This proposal suggests instead that even relocation won't happen, so that's an improvement over them in the case when alias expression usage is applicable.
P0927 also "guarantees copy elision". That proposal requires that the lazy parameter is only used once (to forward it to another lazy function or to its final destination), while in some cases it may be desirable to acquire and use it for some time before forwarding. This proposal would allow to do this in a clean way.