Skip to content

Instantly share code, notes, and snippets.

@Anton3
Last active January 2, 2020 03:44
Show Gist options
  • Save Anton3/f62dde10fdc6c3ae9d21650f54656157 to your computer and use it in GitHub Desktop.
Save Anton3/f62dde10fdc6c3ae9d21650f54656157 to your computer and use it in GitHub Desktop.

Alias expressions

Summary

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.

Motivation

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.

Proposed solution

Alias expression do (A -> B) { C }:

  1. Computes expression A
  2. Binds the result of A to the reference B
  3. Executes the body C, which can use that reference
  4. The result of the alias expression is the result of A

No copies or moves involved!

Examples

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.

Strict definition

An alias expression is a primary expression of the form do (A -> B) C where:

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.

Notes on the interaction with control flow statements

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 of w would be destroyed 👍
  • (3) During stack unwinding, the result object of w would be destroyed 👍
  • (4) w would be destroyed after the expression in the return 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 of w is destroyed. The destruction is sequenced as if that object was the first object declared in the scope.
  • (4) return statements and coroutine co_return, co_await, co_yield operations are not allowed inside the block of an alias expression, including nested blocks.

Q&A

Can we implement similar functionality in C++ today?

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.

Can we make NRVO guaranteed?

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.

Doesn't prvalue-to-lvalue conversion imply temporary materialization?

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

That's too weird to have statements inside an expression

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);
  };
}

Syntax variations

  • do (widget() -> w) { use(w); } as used throughout this proposal
    • ➕ Similar in structure to familiar if, while, for syntax
  • 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.

What about the relocation proposals?

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.

What about the lazy parameters proposal?

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment