Doc. no.: P0849R8
Date: 2021-7-12
Audience: LWG, CWG
Reply-to: Zhihao Yuan <zy at miator dot net>

auto(x): decay-copy in the language

Changes Since R7

Changes Since R6

Changes Since R5

Changes Since R4

Changes Since R3

Changes Since R2

Changes Since R1

Changes Since R0

Introduction

This paper proposes auto(x) and auto{x} for casting x into a prvalue as if passing x as a function argument by value. The functionality appears as the decay-copy function in the standard for exposition only.

Motivation

Obtaining a prvalue copy is necessary

A generic way to obtain a copy of an object in C++ is auto a = x; but such a copy is an lvalue. We could often convey the purpose in code more accurately if we can obtain the copy as a prvalue. In the following example, let Container be a concept,

void pop_front_alike(Container auto& x) {
    std::erase(x.begin(), x.end(), auto(x.front()));
}

If we write

void pop_front_alike(Container auto& x) {
    auto a = x.front();
    std::erase(x.begin(), x.end(), a);
}

, questions arise – why this is not equivalent to

void pop_front_alike(Container auto& x) {
    std::erase(x.begin(), x.end(), x.front());
}

The problem is, the statement to obtain an lvalue copy is a declaration:

    auto a = x.front();

The declaration’s primary purpose is to declare a variable, while the variable being a copy is the declaration’s property. In contrast, the expression to obtain an rvalue copy is a clear command to perform a copy:

    auto(x.front())

One might argue that the above is indifferent from

    T(x.front())

However, there are plenty of situations that the T is nontrivial to get. We probably don’t want to write the original example as

void pop_front_alike(Container auto& x) {
    using T = std::decay_t<decltype(x.front())>;
    std::erase(x.begin(), x.end(), T(x.front()));
}

Obtaining a prvalue copy with auto(x) works always

In standard library specification, we use the following exposition only function to fulfill auto(x)'s role:

template<class T>
constexpr decay_t<T> decay_copy(T&& v) noexcept(
    is_nothrow_convertible_v<T, decay_t<T>>) {
    return std::forward<T>(v);
}

This definition involves templates, dependent constexpr, forwarding reference, noexcept, and two traits, and still has caveats if people want to use it in practice. An obvious issue is that decay_copy(x.front()) copies x.front() even if x.front() is a prvalue, in other words, a copy.

There is a less obvious issue that needs a code snippet to reproduce:

class A {
    int x;

public:
    A();

    auto run() {
        f(A(*this));           // ok
        f(auto(*this));        // ok as proposed
        f(decay_copy(*this));  // ill-formed
    }

protected:
    A(const A&);
};

The problem is that decay_copy is nobody’s friend. We can use A directly in this specific example. However, in a more general setting, where a type has access to a set of type T’s private or protected copy/move constructors, decay-copy an object of T fails inside that type’s class scope, but auto(x) continues to work.

Discussion

auto(x) is a missing piece

Replacing the char in char('a') with auto, we obtain auto('a'), which is a function-style cast. Such a formula also supports injected-class-names and class template argument deduction in C++17. Introducing auto(x) and auto{x} significantly improves the language consistency:

variable definition function-style cast new expression
auto v(x); auto(x) new auto(x)
auto v{x}; auto{x} new auto{x}
ClassTemplate v(x); ClassTemplate(x) new ClassTemplate(x)
ClassTemplate v{x}; ClassTemplate{x} new ClassTemplate{x}

** The type of x is a specialization of ClassTemplate.

With this proposal, all the cells in the table copy construct form x (due to CTAD’s default behavior) to obtain lvalues, prvalues, and pointers to objects, categorized by their columns. Defining auto(x) as a library[1] facility loses orthogonality.

Introducing auto(x) into the language even improves the library consistency:

type function style expression style
void_t<decltype(expr)> decltype(void(expr))
decay_t<decltype(expr)> decltype(auto(expr))

Note that auto(expr) does not turn a move-only lvalue expr into a prvalue regardless whether auto(expr) appears in an immediate context, so there is some difference between decltype(auto(expr)) and decay_t<decltype(expr)>.

Do we also miss decltype(auto){x}?

decltype(auto){arg} can forward arg without computing arg’s type. It is equivalent to static_cast<decltype(arg)>(arg) . If arg is a variable of type T&&, arg is an lvalue but static_cast<T&&>(arg) is an xvalue.

EWG discussed this idea, disliked its expert-friendly nature, and concluded that adding this facility would cause the teaching effort to add up.

Does auto works in place of decay-copy in the library specification?

Not as a simple find-and-replace, but can be made to improve the quality of the library specification.

The background is that, despite being exposition-only, decay-copy always materializes its argument and produces a copy. auto(expr) is a no-op if the expr is a prvalue.

In the library specification where uses decay-copy, some do not mean to materialize the expressions; some want a new copy; some do not care. However, with auto(x) semantics, we should distinguish the different needs and explicitly say so when a copy is needed.

What’s the difference between decltype(auto(expr)) and decay_t?

decay_t<decltype(expr)> does not look at expr’s value category, so the following can hold:

auto p = make_unique<char>();
static_assert(is_same_v<decay_t<decltype(p)>, unique_ptr<char>>);

decay_t<U&> will also simply strips & even if U is a move-only type.

As a result, the decay-copy exposition-only function is, strictly speaking, insufficiently constrained:

auto p2 = decay_copy(p);  // fails inside definition

The situation where decltype(decay_copy(expr)) does not fail in an immediate context does not imply that the call decay_copy(expr) is well-formed.

decltype(auto(expr)) has no such problem. But this does not necessarily mean that decltype(auto(expr)) is better than decay_t or something. Sometimes you do want to compute a “decayed-type,” and sometimes, you may find it helpful to exclude the expressions that cannot be passed as arguments to a parameter declared auto.

May a different spelling work better, such as prvalue_cast?

First, auto(x) casts to prvalue, but may not be how the name prvalue_cast suggests. If there is a prvalue_cast keyword, you may expect the following code to pass an array prvalue to the function foo:

double v[] = { 1.2, 3.5, .8 };
foo(prvalue_cast(v));

But

foo(auto(v));

will pass a double*. Therefore, we are talking about a different facility.

On the other hand, the term “prvalue” is unfamiliar to ordinary C++ users. While some people may expect a prvalue-casting without decaying, some other people may not have any expectation when seeing such a term.

In contrast, the use of the auto keyword implies that this expression decays. It decays in the same way

auto p = v;

does and in the same way

void bar(auto v);
bar(v);

does. This consistency makes the proposed spelling a lot more teachable comparing to the others.

Can we build a decay_copy with a metaprogram that forwards prvalue?

P2237 Metaprogramming means to replace macros, and the answer is yes. Barry kindly provides the following snippet:

consteval void prvalue_cast(meta::info expr) {
    -> decay_t<|type_of(expr)|>(|expr|);
}

However, we should note that we added auto type-specifier in declarations and new expressions not because we cannot meta-program in those contexts. We added them because they are useful, so does auto in a function-style cast, as shown in this paper.

As a side note, it may be a fun thing to see if someone comes up with the following code in the future:

auto p = new auto(prvalue_cast(v));

Should we also make static_cast<auto>(x) work?

The motivation is that because T(x) is indifferent from (T)x – a C-style cast that may try every possible route to T, some codebases may ban auto(x) altogether with T(x) even though auto is a type-placeholder rather than a type. static_cast<auto>(x) can be a way to evacuate from such a check.

In terms of styles, without loss of generality, char{x} means direct-list-initialization from x. It allows no narrowing and is not a cast. I believe that no codebase would ban T{x}, so auto{x} can be an alternative spelling if auto(x) is accidentally banned.

If we ignore the motivation for a moment, I think:

  1. Whether static_cast<auto>(x) should work should also take static_cast<template-id>(x) (CTAD) into account. The latter is currently forbidden.
  2. If we have such a paper, we may also want to decide whether static_cast<auto&&>(x) should work. This expression is a std::forward without a need to supply the type, but EWG has already turned down a similar idea (decltype(auto){x}).

Relation with other papers

P2255R0[2]A type trait to detect reference binding to temporary” guards std::tuple and std::pair constructors from forming references that bind to temporaries. The change is a good thing because it can make the following code fail to compile:

tuple<const string&> x(auto(obj.get_str()));

auto(x) can make copies if necessary, but this does not imply that “it is safer” without a context.

P0847R6[3] “Deducing this” introduced by-value member functions. With those, a call to a member function may always copy the object:

foo(vec.sorted());

Combining it with auto(obj) has no cost:

foo(auto(vec).sorted());

You can also use this style when calling some member functions that are unwillingly modifying the implicit object parameters:

foo(auto(vec).sorted_in_place());

Demo

Prevent algorithm from modifying through aliases: https://godbolt.miator.net/z/hhcvbc

Using auto(x) in rvalue fluent interface: https://godbolt.miator.net/z/TY8sxr

How auto(x) assists in defining concepts: https://godbolt.miator.net/z/GTaaeE

Compare diagnosis to new auto(x): https://godbolt.miator.net/z/Ks43an

Wording

The wording is relative to N4892.

Part 1

Modify 7.6.1.4 [expr.type.conv]/1 as indicated:

A simple-type-specifier (9.2.9.3) or typename-specifier (13.8) followed by a parenthesized optional expression-list or by a braced-init-list (the initializer) constructs a value of the specified type given the initializer. If the type is a placeholder for a deduced class type, it is replaced by the return type of the function selected by overload resolution for class template deduction (12.2.2.9) for the remainder of this section. Otherwise, if the type is auto, it is replaced by the type deduced for the variable x in the invented declaration ([dcl.spec.auto]), which is never interpreted as a function declaration:

    auto x init;

, where init is the initializer. [Example:
  struct A {};
  void f(A&);  // #1
  void f(A&&); // #2
  A& g();
  void h() {
    f(g());        // calls #1
    f(A(g()));     // calls #2 with a temporary object
    f(auto(g()));  // calls #2 with a temporary object
  }
— end example]

Modify 9.2.9.6.1 [dcl.spec.auto]/5 as indicated:

A placeholder type can also be used in the type-specifier-seq in the new-type-id or type-id of a new-expression (7.6.2.8) and as a decl-specifier of the parameter-declaration’s decl-specifier-seq in a template-parameter (13.2). The auto type-specifier can also be used as the simple-type-specifier in an explicit type conversion (functional notation) ([expr.type.conv]).

Part 2

[Drafting note: Here is a summary of patterns in the library changes:

Description Before Proposed Alternative
decay-copy specific expression decay-copy(begin(t)) auto(begin(t)) -
decay-copy unspecific expression decay-copy(E) unchanged auto(identity()(E))
explain aftermath of evaluating decay-copy calls to decay-copy being evaluated in the constructing thread values produced by auto being materialized in the constructing thread -
There are three uses of decay-copy that this wording does not propose to change. They are in [range.all.general], [range.take], and [range.drop].
–end note]

Modify 24.3.2 [range.access.begin]/2 as indicated:

Given a subexpression E with type T, let t be an lvalue that denotes the reified object for E. Then:

Modify 24.3.3 [range.access.end]/2 as indicated:

Given a subexpression E with type T, let t be an lvalue that denotes the reified object for E. Then:

Modify 24.3.6 [range.access.rbegin]/2 as indicated:

Given a subexpression E with type T, let t be an lvalue that denotes the reified object for E. Then:

Modify 24.3.7 [range.access.rend]/2 as indicated:

Given a subexpression E with type T, let t be an lvalue that denotes the reified object for E. Then:

Modify 24.3.10 [range.prim.size]/2 as indicated:

Given a subexpression E with type T, let t be an lvalue that denotes the reified object for E. Then:

Modify 24.3.13 [range.prim.data]/2 as indicated:

Given a subexpression E with type T, let t be an lvalue that denotes the reified object for E. Then:

Modify 32.4.3.3 [thread.thread.constr]/5 as indicated:

Effects: The new thread of execution executes

  invoke(decay-copyauto(std::forward<F>(f)),
   decay-copyauto(std::forward<Args>>(args))…)

with the calls to decay-copy being evaluatedvalues produced by auto being materialized ([conv.rval]) in the constructing thread. Any return value from this invocation is ignored. […]

Modify 32.4.4.2 [thread.jthread.cons]/5 as indicated:

Effects: Initializes ssource. The new thread of execution executes

  invoke(decay-copyauto(std::forward<F>(f)), get_stop_token(),
   decay-copyauto(std::forward<Args>>(args))…)

if that expression is well-formed, otherwise

  invoke(decay-copyauto(std::forward<F>(f)),
   decay-copyauto(std::forward<Args>>(args))…)

with the calls to decay-copy being evaluatedvalues produced by auto being materialized ([conv.rval]) in the constructing thread. Any return value from this invocation is ignored. […]

Modify 32.9.9 [futures.async]/3 as indicated:

Effects: The first function behaves the same as a call to the second function with a policy argument of launch::async | launch::deferred […]:

Acknowledgments

Thank Alisdair Meredith, Arthur O’Dwyer, and Billy O’Neal for providing examples and feedback for this paper. Thank James Touton for presenting the paper and bringing it forward. Thank Jens Maurer and Casey Carter for reviewing the wording.

References


  1. Krügler, Daniel. P0758R0 Implicit conversion traits and utility functions. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0758r0.html ↩︎

  2. Song, Tim. P2255R0 A type trait to detect reference binding to temporary. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2255r0.html ↩︎

  3. Ažman et al. P0847R6 Deducing this. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0847r6.html ↩︎