| Doc. no.: | P0849R4 | 
|---|
| Date: | 2020-10-10 | 
|---|
| Audience: | EWG, LWG | 
|---|
| Reply-to: | Zhihao Yuan <zy at miator dot net> | 
|---|
auto(x): decay-copy in the language
Changes Since R3
Changes Since R2
- dropped decltype(auto)(x)in comply with EWG’s opinion
Changes Since R1
- propose decltype(auto)(x)as well
Changes Since R0
- updated examples
- discussed decltype(auto)(x)
- added library wording
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 wrote
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 minimal reproduce:
class A {
    int x;
public:
    A();
    auto run() {
        f(A(*this));           
        f(auto(*this));        
        f(decay_copy(*this));  
    }
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 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)) | 
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.
Implementation
Try it out: Godbolt
Wording
The wording is relative to N4861.
Part 1
Modify 7.6.1.3 [expr.type.conv]/1 as indicated:
A simple-type-specifier (9.2.8.2) 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.4.1.8) 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 (9.2.8.5):
auto x init;
, where  init is the initializer.
Modify 9.2.8.5 [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.7) 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) (7.6.1.3).
Part 2
Remove the first entity from 16.4.2.1 [expos.only.func]/2:
template<class T> constexpr decay_t<T> decay-copy(T&& v)
    noexcept(is_nothrow_convertible_v<T, decay_t<T>>)     // exposition only
  { return std::forward<T>(v); }
Modify 24.3.1 [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:
- If Eis an rvalue andenable_borrowed_range<remove_cv_t<T>>isfalse,ranges::begin(E)is ill-formed.
- Otherwise, if Tis an array type (6.8.2) andremove_all_extents_t<T>is an incomplete type,ranges::begin(E)is ill-formed with no diagnostic required.
- Otherwise, if Tis an array type,ranges::begin(E)is expression-equivalent tot + 0.
- Otherwise, if decay-copy
auto(t.begin())is a valid expression whose type modelsinput_or_output_iteratr,ranges::begin(E)is expression-equivalent todecay-copy
auto(t.begin()).
- Otherwise, if Tis a class or enumeration type anddecay-copy
auto(begin(t))is a valid expression whose type modelsinput_or_output_iteratorwith overload resolution performed in a context in which unqualified lookup forbeginfinds only the declarations
 void begin(auto&) = delete;
 void begin(const auto&) = delete;
 thenranges::begin(E)is expression-equivalent todecay-copy
auto(begin(t))with overload resolution performed in the above context.
- Otherwise, ranges::begin(E)is ill-formed.
Modify 24.3.2 [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:
- If Eis an rvalue andenable_borrowed_range<remove_cv_t<T>>isfalse,ranges::end(E)is ill-formed.
- Otherwise, if Tis an array type (6.8.2) andremove_all_extents_t<T>is an incomplete type,ranges::end(E)is ill-formed with no diagnostic required.
- Otherwise, if Tis an array of unknown bound,ranges::end(E)is ill-formed.
- Otherwise, if Tis an array,ranges::end(E)is expression-equivalent tot + extent_v<T>.
- Otherwise, if decay-copy
auto(t.end())is a valid expression whose type modelssentinel_for<iterator_t<T>>thenranges::end(E)is expression-equivalent todecay-copy
auto(t.end()).
- Otherwise, if Tis a class or enumeration type anddecay-copy
auto(end(t))is a valid expression whose type modelssentinel_for<iterator_t<T>>with overload resolution performed in a context in which unqualified lookup forendfinds only the declarations
 void end(auto&) = delete;
 void end(const auto&) = delete;
 thenranges::end(E)is expression-equivalent todecay-copy
auto(end(t))with overload resolution performed in the above context.
- Otherwise, ranges::end(E)is ill-formed.
Modify 24.3.5 [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:
- If Eis an rvalue andenable_borrowed_range<remove_cv_t<T>>isfalse,ranges::rbegin(E)is ill-formed.
- Otherwise, if Tis an array type (6.8.2) andremove_all_extents_t<T>is an incomplete type,ranges::rbegin(E)is ill-formed with no diagnostic required.
- Otherwise, if decay-copy
auto(t.rbegin())is a valid expression whose type modelsinput_or_output_iteratr,ranges::rbegin(E)is expression-equivalent todecay-copy
auto(t.rbegin()).
- Otherwise, if Tis a class or enumeration type anddecay-copy
auto(rbegin(t))is a valid expression whose type modelsinput_or_output_iteratorwith overload resolution performed in a context in which unqualified lookup forrbeginfinds only the declarations
 void rbegin(auto&) = delete;
 void rbegin(const auto&) = delete;
 thenranges::rbegin(E)is expression-equivalent todecay-copy
auto(rbegin(t))with overload resolution performed in the above context.
- […]
Modify 24.3.6 [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:
- If Eis an rvalue andenable_borrowed_range<remove_cv_t<T>>isfalse,ranges::rend(E)is ill-formed.
- Otherwise, if Tis an array type (6.8.2) andremove_all_extents_t<T>is an incomplete type,ranges::rend(E)is ill-formed with no diagnostic required.
- Otherwise, if decay-copy
auto(t.rend())is a valid expression whose type modelssentinel_for<decltype(ranges::rbegin(E)>thenranges::rend(E)is expression-equivalent todecay-copy
auto(t.rend()).
- Otherwise, if Tis a class or enumeration type anddecay-copy
auto(rend(t))is a valid expression whose type modelssentinel_for<decltype(ranges::rbegin(E)>with overload resolution performed in a context in which unqualified lookup forrendfinds only the declarations
 void rend(auto&) = delete;
 void rend(const auto&) = delete;
 thenranges::rend(E)is expression-equivalent todecay-copy
auto(rend(t))with overload resolution performed in the above context.
- […]
Modify 24.3.9 [range.access.size]/2 as indicated:
Given a subexpression E with type T, let t be an lvalue that denotes the reified object for E. Then:
- If Tis an array of unknown bound (9.3.3.4),ranges::size(E)is ill-formed.
- Otherwise, if Tis an array type,ranges::size(E)is expression-equivalent todecay-copy
auto(extent_v<T>).
- Otherwise, if disable_sized_range<remove_cv_t<T>>(24.4.3) isfalseanddecay-copy
auto(t.size())is a valid expression of integer-like type (23.3.4.4),ranges::size(E)is expression-equivalent todecay-copy
auto(t.size()).
- Otherwise, if Tis a class or enumeration type,disable_sized_range<remove_cv_t<T>>isfalseanddecay-copy
auto(size(t))is a valid expression of integer-like type with overload resolution performed in a context in which unqualified lookup forsizefinds only the declarations
 void size(auto&) = delete;
 void size(const auto&) = delete;
 thenranges::size(E)is expression-equivalent todecay-copy
auto(size(t))with overload resolution performed in the above context.
- […]
Modify 24.3.12 [range.access.data]/2 as indicated:
Given a subexpression E with type T, let t be an lvalue that denotes the reified object for E. Then:
- If Eis an rvalue andenable_borrowed_range<remove_cv_t<T>>isfalse,ranges::data(E)is ill-formed.
- Otherwise, if Tis an array type (6.8.2) andremove_all_extents_t<T>is an incomplete type,ranges::data(E)is ill-formed with no diagnostic required.
- Otherwise, if decay-copy
auto(t.data())is a valid expression of pointer to object type,ranges::data(E)is expression-equivalent todecay-copy
auto(t.data()).
- […]
Modify 24.7.3 [range.all]/2 as indicated:
The name views::all denotes a range adaptor object (24.7.1). Given a subexpression E, the expression views::all(E) is expression-equivalent to:
- decay-copy
- auto- (E)if- the decayed type of - E
the type of that expression models- view.
- Otherwise, ref_view{E}if that expression is well-formed.
- Otherwise, subrange{E}.
Modify 24.7.6.1 [range.take.overview]/2 as indicated:
The name views::take denotes a range adaptor object (24.7.1). Let E and F be expressions, let T be remove_cvref_t<decltype((E))>, and let D be range_difference_t<decltype((E))>. If decltype((F)) does not model convertible_to<D>, views::take(E, F) is ill-formed. Otherwise, the expression views::take(E, F) is expression-equivalent to:
- If Tis a specialization ofranges::empty_view(24.6.1.2), then((void) F, decay-copy
auto(E)).
- […]
Modify 24.7.8.1 [range.drop.overview]/2 as indicated:
The name views::drop denotes a range adaptor object (24.7.1). Let E and F be expressions, let T be remove_cvref_t<decltype((E))>, and let D be range_difference_t<decltype((E))>. If decltype((F)) does not model convertible_to<D>, views::drop(E, F) is ill-formed. Otherwise, the expression views::drop(E, F) is expression-equivalent to:
- If Tis a specialization ofranges::empty_view(24.6.1.2), then((void) F, decay-copy
auto(E)).
- […]
Modify 32.4.2.2 [thread.thread.constr]/6 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 in the constructing thread. Any return value from this invocation is ignored. […]
Modify 32.4.3.1 [thread.jthread.cons]/6 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 in the constructing thread. Any return value from this invocation is ignored. […]
Modify 32.9.9 [futures.async]/4 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 […]:
- If launch::asyncis set inpolicy, callsinvoke(decay-copyauto(std::forward<F>(f)),decay-copyauto(std::forward<Args>>(args))…)
(20.14.3, 32.4.2.2) as if in a new thread of execution represented by athreadobject with thecalls to decay-copybeing evaluated
values produced byautobeing materialized in the thread that calledasync. Any return value is stored as the result in the shared state. Any exception propagated from the execution ofinvoke(decay-copyauto(std::forward<F>(f)),decay-copyauto(std::forward<Args>>(args))…)
is stored as the exceptional result in the shared state. Thethreadobject is stored in the shared state and affects the behavior of any asynchronous return objects that
reference that state.
- If launch::deferredis set inpolicy, storesdecay-copy
auto(std::forward<F>(f))anddecay-copy
auto(std::forward<Args>(args))...in the shared state. These copies offandargsconstitute a deferred function. Invocation of the deferred function evaluatesinvoke(std::move(g), std::move(xyz))wheregis the stored value ofdecay-copy
auto(std::forward<F>(f))andxyzis the stored copy ofdecay-copy
auto(std::forward<Args>(args)).... Any return value is stored as
the result in the shared state. Any exception propagated from the execution of the deferred
function is stored as the exceptional result in the shared state. […]
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.
References
auto(x): decay-copy in the language
Changes Since R3
Changes Since R2
decltype(auto)(x)in comply with EWG’s opinionChanges Since R1
decltype(auto)(x)as wellChanges Since R0
decltype(auto)(x)Introduction
This paper proposes
auto(x)andauto{x}for castingxinto a prvalue as if passingxas a function argument by value. The functionality appears as thedecay-copyfunction 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, letContainerbe a concept,void pop_front_alike(Container auto& x) { std::erase(x.begin(), x.end(), auto(x.front())); }If we wrote
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
Tis nontrivial to get. We probably don’t want to write the original example asvoid 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 alwaysIn 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 thatdecay_copy(x.front())copiesx.front()even ifx.front()is a prvalue, in other words, a copy.There is a less obvious issue that needs a minimal 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_copyis nobody’s friend. We can useAdirectly in this specific example. However, in a more general setting, where a type has access to a set of typeT's private or protected copy/move constructors,decay-copyan object ofTfails inside that type’s class scope, butauto(x)continues to work.Discussion
auto(x)is a missing pieceReplacing the
charinchar('a')withauto, we obtainauto('a'), which is a function-style cast. Such a formula also supports injected-class-names and class template argument deduction in C++17. Introducingauto(x)andauto{x}significantly improves the language consistency:** The type of
xis a specialization ofClassTemplate.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. Definingauto(x)as a library[1] facility loses orthogonality.Introducing
auto(x)into the language even improves the library consistency:Do we also miss
decltype(auto){x}?decltype(auto){arg}can forwardargwithout computingarg's type. It is equivalent tostatic_cast<decltype(arg)>(arg). Ifargis a variable of typeT&&,argis an lvalue butstatic_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.
Implementation
Try it out: Godbolt
Wording
The wording is relative to N4861.
Part 1
Modify 7.6.1.3 [expr.type.conv]/1 as indicated:
Modify 9.2.8.5 [dcl.spec.auto]/5 as indicated:
Part 2
Remove the first entity from 16.4.2.1 [expos.only.func]/2:
Modify 24.3.1 [range.access.begin]/2 as indicated:
Modify 24.3.2 [range.access.end]/2 as indicated:
Modify 24.3.5 [range.access.rbegin]/2 as indicated:
Modify 24.3.6 [range.access.rend]/2 as indicated:
Modify 24.3.9 [range.access.size]/2 as indicated:
Modify 24.3.12 [range.access.data]/2 as indicated:
Modify 24.7.3 [range.all]/2 as indicated:
Modify 24.7.6.1 [range.take.overview]/2 as indicated:
Modify 24.7.8.1 [range.drop.overview]/2 as indicated:
Modify 32.4.2.2 [thread.thread.constr]/6 as indicated:
Modify 32.4.3.1 [thread.jthread.cons]/6 as indicated:
Modify 32.9.9 [futures.async]/4 as indicated:
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.
References
Krügler, Daniel. P0758R0 Implicit conversion traits and utility functions. http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0758r0.html ↩︎