Why "Universal References" Have the Same Syntax as Rvalue References

C++ universal references. Why rvalue reference becomes lvalue?

Think about it this way, you used std::forward<T> in func, so likewise, to make sure the parameter is forwarded as an Rvalue reference, you have to do the same in the recursive function:

void testRef(int && param)
{
std::cout << "Rvalue reference" << std::endl;

// Here's the thing I can't get. Why param is lvalue reference here??
testRef( param );

testRef(std::forward<int &&>(param)); // now it will stay an Rvalue reference
testRef(std::move(param)); // make it an Rvalue reference
}

The reason we need std::forward or std::move is because param is of type int&& which is an lvalue (i.e. an rvalue reference parameter is an lvalue expression when you use it).

Behind the scenes these templates will eventually perform a static_cast<int &&> which yields an xvalue expression (which is also classified as an rvalue expression.) An xvalue expression binds to rvalue reference parameters.

This can be seen by looking at Clang's syntax tree for the following function:

             rvalue reference parameter (which binds to rvalue expressions)
vvvvvvvvvvv
void testRef(int&& param)
{
//std::move(param);

lvalue expression of type int&&
vvvvv
static_cast<int &&>(param);
^^^^^^^^^^^^^^^^^^^^^^^^^^
xvalue expression
(considered an rvalue expression which binds to rvalue reference parameters)
}

Abstract syntax tree for the function above:

TranslationUnitDecl
`-FunctionDecl <line:3:1, line:7:1> line:3:6 testRef 'void (int &&)'
|-ParmVarDecl <col:14, col:21> col:21 used param 'int &&'
`-CompoundStmt <line:4:1, line:7:1>
`-CXXStaticCastExpr <line:6:5, col:30> 'int' xvalue static_cast<int &&> <NoOp>
`-DeclRefExpr <col:25> 'int' lvalue ParmVar 0x55a692bb0a90 'param' 'int &&'

A shorthand way of explaining that a reference parameter becomes an lvalue would be to say that when it has a name (id-expression) it is an lvalue.

Syntax for universal references

A universal reference such as T&& can deduce T to be an "object type", or a "reference type"

In your example it can deduce T as int when passed an rvalue, so the function parameter is int&&, or it can deduce T as int& when passed an lvalue, in which case the function parameter is int& (because the reference collapsing rules say std::add_rvalue_reference<int&>::type is just int&)

If T isn't deduced by the function call (as in your X::baz example) then it can't be deduced to int&, so the reference isn't a universal reference.

So IMHO there's really no need for new syntax, it fits nicely into template argument deduction and reference collapsing rules, with the small tweak that a template parameter can be deduced as a reference type (where in C++03 a function template parameter of type T or T& would always deduce T as an object type.)

These semantics and this syntax were proposed right from the beginning when rvalue references and a tweak to the argument deduction rules were proposed as the solution to the forwarding problem, see N1385. Using this syntax to provide perfect forwarding was proposed in parallel with proposing rvalue references for the purposes of move semantics: N1377 was in the same mailing as N1385. I don't think an alternative syntax was ever seriously proposed.

IMHO an alternative syntax would actually be more confusing anyway. If you had template<typename T> void bar(T&@) as the syntax for a universal reference, but the same semantics as we have today, then when calling bar(i) the template parameter T could be deduced as int& or int and the function parameter would be of type int& or int&& ... neither of which is "T&@" (whatever that type is.) So you'd have grammar in the language for a declarator T&@ which is not a type that can ever exist, because it actually always refers to some other type, either int& or int&&.

At least with the syntax we've got the type T&& is a real type, and the reference collapsing rules are not specific to function templates using universal references, they're completely consistent with the rest of the type system outside of templates:

struct A {} a;
typedef A& T;
T&& ref = a; // T&& == A&

Or equivalently:

struct A {} a;
typedef A& T;
std::add_rvalue_reference<T>::type ref = a; // type == A&

When T is an lvalue reference type, T&& is too. I don't think a new syntax is needed, the rules really aren't that complicated or confusing.

Is overloading on universal references now much safer with concepts in c++ 20

I would say no. I mean, concepts help, because the syntax is nicer than what we had before, but it's still the same problem.

Here's a real-life example: std::any is constructible from any type that is copy constructible. So there you might start with:

struct any {
template <class T>
requires std::copy_constructible<std::decay_t<T>>
any(T&&);

any(any const&);
};

The problem is, when you do something like this:

any a = 42; // calls any(T&&), with T=int
any b = a; // calls any(T&&), with T=any

Because any itself is, of course, copy constructible. This makes the constructor template viable, and a better match since it's a less-const-qualified reference.

So in order to avoid that (because we want b to hold an int, and not hold an any that holds an int) we have to remove ourselves from consideration:

struct any {
template <class T>
requires std::copy_constructible<std::decay_t<T>>
&& (!std::same_as<std::decay_t<T>, any>)
any(T&&);

any(any const&);
};

This is the same thing we had to do in C++17 and earlier when Scott Meyers wrote his book. At least, it's the same mechanism for resolving the problem - even if the syntax is better.

Have rvalue reference instead of forwarding reference with variadic template

The way to have a bunch of rvalue references is with SFINAE:

template <class... Args,
std::enable_if_t<(!std::is_lvalue_reference<Args>::value && ...), int> = 0>
void foo(Args&&... args) { ... }

Fold-expressions are C++17, it is easy enough to write a metafunction to get that same behavior in C++14. This is your only option really - you want a constrained function template deducing to rvalue references, but the only available syntax is overloaded to mean forwarding references. We could make the template parameters non-deduced, but then you'd have to provide them, which seems like not a solution at all.

With concepts, this is of course cleaner, but we're not really changing the underlying mechanism:

template <class... Args>
requires (!std::is_lvalue_reference<Args>::value && ...)
void foo(Args&&... args) { ... }

or better:

template <class T>
concept NonReference = !std::is_lvalue_reference<T>::value;

template <NonReference... Args>
void foo(Args&&... ) { ... }

It's worth pointing out that neither of these work:

template <class... Args> auto foo(const Args&... args) = delete;
template <class... Args> auto foo(Args&... args) = delete;

because they only delete overloads that take all lvalue references, and you want to delete overloads that take any lvalue references.

C++11: Why rvalue reference parameter implicitly converted to lvalue

One, the x argument to fn isn't an r-value reference, it's a "universal reference" (yes, this is rather confusing).

Two, the moment you give an object a name, that name is not an r-value unless explicitly "fixed", either with std::move (to make it an r-value reference, always), or with std::forward (to convert it back to its original type in the case of universal references). If you want to avoid the complaint, use std::forward to forward as the original type:

template <class T>
void fn (T&& x) {
overloaded(std::forward<T>(x));
}

Do rvalue references have the same overhead as lvalue references?

In your specific situation, it's likely they are all the same. The resulting code from godbolt with gcc -O3 is https://godbolt.org/g/XQJ3Z4 for:

#include <utility>

// runtime dominated by argument passing
template <class T>
int foo(T t) { return t;}

int main() {
int i{0};
volatile int j;
j = foo<int>(i); // fast -- int is scalar type
j = foo<int&>(i); // slow -- lvalue reference overhead
j = foo<int&&>(std::move(i)); // ???
}

is:

    mov     dword ptr [rsp - 4], 0 // foo<int>(i);
mov dword ptr [rsp - 4], 0 // foo<int&>(i);
mov dword ptr [rsp - 4], 0 // foo<int&&>(std::move(i));
xor eax, eax
ret

The volatile int j is so that the compiler cannot optimize away all the code because it would otherwise know that the results of the calls are discarded and the whole program would optimize to nothing.

HOWEVER, if you force the function to not be inlined, then things change a bit int __attribute__ ((noinline)) foo(T t) { return t;}:

int foo<int>(int):                           # @int foo<int>(int)
mov eax, edi
ret
int foo<int&>(int&): # @int foo<int&>(int&)
mov eax, dword ptr [rdi]
ret
int foo<int&&>(int&&): # @int foo<int&&>(int&&)
mov eax, dword ptr [rdi]
ret

above: https://godbolt.org/g/pbZ1BT

For questions like these, learn to love https://godbolt.org and https://quick-bench.com/ (quick bench requires you to learn how to properly use google test)

C++, rvalue references in function parameters

Like @Peter said, the type of T is deduced as string&, and C++’s reference-collapsing rule says:

T& & ⇒ T& // from C++98

T&& & ⇒ T& // new for C++0x

T& && ⇒ T& // new for C++0x

T&& && ⇒ T&& // new for C++0x

So func’s instantiation is actually:

void func(string& str)

And it works.

What's the standard/official name for universal references?

Overview

It is known that since C++11, a parameter of type T&& is called an rvalue reference [ISO/IEC 14882:2011 §8.3.2/p2 References [dcl.ref] ]. That is, unless T is a template parameter type or auto or a typedef for some lvalue reference type.

examples:

template<typename T>
void foo(T&& p) { // -> T is a template parameter
...
}

auto &&p = expression;

Although technically T&& in the examples above is still an rvalue reference, its behaviour differs significantly from a regular one.

Naturally, you would ask "why this special case doesn't have a special syntax". The answer is that the && syntax was intentionally overloaded for this special construct by the C++ committee. However, they missed to name this special case.

In the absence of a distinct name for this particular construct, Scott Meyers coined the widely known term/name universal references.

The committee however, decided that this name is not proper for a number of reasons. As such, the proposal N4164 made by Herb Sutter, Bjarne Stroustrup and Gabriel Dos Reis proposed to change the name to Forwarding References.

The name Forwarding References had the most support in informal discussions among committee members, including the authors of the proposal mentioned earlier. Interestingly enough, it was Scott Meyers himself that introduced that term in his original “Universal References” talk. However, later he decided to go along with the name universal references. For this decision played role the fact that at the time he didn't think that the term forwarding references included also the auto&& case.

Why not universal references?

According to the proposal the term Universal references although is a reasonable name with an obvious meaning, it happens to be wrong in several aspects.

A universal reference must mean the following:

  • A reference that can be used everywhere; or
  • A reference that can be used for everything; or
  • something similar.

Obviously this is not the case nor is the appropriate use of this construct. Furthermore, this name would encourage many people to consider that something having such a name is meant to be used "universally". Something that the committee considered it a bad thing.

Moreover, "universal references" aren’t even really
references per se, but rather a set of rules for using references in a particular way in a particular context with some language support for that use, and that use is forwarding.

Why auto&& is Also Considered a Forwarding case

auto&& is also considered a forward case since it follows the reference collapsing rules. For example in:

  • Generic lambdas of the form, [](auto&& x){ … }
  • for-ranged loop of the form, for(auto &&i : v) { ... }
  • Finally, in general is true that auto&& local variables are for forwarding.

Standard Wordings for Forwarding References

The term forwarding references is mentioned in the draft standard N4527 in the following places:

§14.8.2.1/ Deducing template arguments from a function call [temp.deduct.call] (Emphasis Mine):

If P is a cv-qualified type, the top level cv-qualifiers of P’s type
are ignored for type deduction. If P is a reference type, the type
referred to by P is used for type deduction. A forwarding
reference
is an rvalue reference to a cv-unqualified template
parameter. If P is a forwarding reference and the argument is an
lvalue, the type “lvalue reference to A” is used in place of A for
type deduction. [ Example:

template <class T> int f(T&& heisenreference);
template <class T> int g(const T&&);
int i;
int n1 = f(i); // calls f<int&>(int&)
int n2 = f(0); // calls f<int>(int&&)
int n3 = g(i); // error: would call g<int>(const int&&), which
// would bind an rvalue reference to an lvalue

— end example ]

§14.8.2.5/p10 Deducing template arguments from a type [temp.deduct.type]:

Similarly, if P has a form that contains (T), then each parameter type
Pi of the respective parameter-typelist of P is compared with the
corresponding parameter type Ai of the corresponding
parameter-type-list of A. If P and A are function types that
originated from deduction when taking the address of a function
template (14.8.2.2) or when deducing template arguments from a
function declaration (14.8.2.6) and Pi and Ai are parameters of the
top-level parameter-type-list of P and A, respectively, Pi is adjusted
if it is a forwarding reference (14.8.2.1) and Ai is an lvalue
reference, in which case the type of Pi is changed to be the template
parameter type (i.e., T&& is changed to simply T). [ Note: As a
result, when Pi is T&& and Ai is X&, the adjusted Pi will be T,
causing T to be deduced as X&. — end note ] [Example:

template <class T> void f(T&&);
template <> void f(int&) { } // #1
template <> void f(int&&) { } // #2
void g(int i) {
f(i); // calls f<int&>(int&), i.e., #1
f(0); // calls f<int>(int&&), i.e., #2
}

— end example ] If the parameter-declaration corresponding to Pi is a
function parameter pack, then the type of its declaratorid is compared
with each remaining parameter type in the parameter-type-list of A.
Each comparison deduces template arguments for subsequent positions in
the template parameter packs expanded by the function parameter pack.
During partial ordering (14.8.2.4), if Ai was originally a function
parameter pack:

How will concepts lite interact with universal references?

It all depends on how the concept itself is written. Concepts-Lite itself (latest TS as of this writing) is agnostic on the matter: it defines mechanisms by which concepts may be defined and used in the language, but does not add stock concepts to the library.

On the other hand document N4263 Toward a concept-enabled standard library is a declaration of intent by some members of the Standard Committee that suggests the natural step after Concepts-Lite is a separate TS to add concepts to the Standard Library with which to constrain e.g. algorithms.

That TS may be a bit far down along the road, but we can still take a look at how concepts have been written so far. Most examples I’ve seen somewhat follow a long tradition where everything revolves around a putative, candidate type that is usually not expected to be a reference type. For instance some of the older Concepts-Lite drafts (e.g. N3580) mention concepts such as Container which have their roots in the SGI STL and survive even today in the Standard Library in the form of 23.2 Container requirements.

A telltale pre-forwarding reference sign is that associated types are described like so:

Value type X::value_type The type of the object stored in a container. The value type must be Assignable, but need not be DefaultConstructible.

If we translate this naïvely to Concepts-Lite, it could look like:

template<typename X>
concept bool Container = requires(X x) {
typename X::value_type;
// other requirements...
};

In which case if we write

template<typename C>
requires Container<C>
void example(C&& c);

then we have the following behavior:

std::vector<int> v;

// fine
// this checks Container<std::vector<int>>, and finds
// std::vector<int>::value_type
example(std::move(v));

// not fine
// this checks Container<std::vector<int>&>, and
// references don't have member types
example(v);

There are several ways to express the value_type requirement which handles this situation gracefully. E.g. we could tweak the requirement to be typename std::remove_reference_t<X>::value_type instead.

I believe the Committee members are aware of the situation. E.g. Andrew Sutton leaves an insightful comment in a concept library of his that showcases the exact situation. His preferred solution is to leave the concept definition to work on non-reference types, and to remove the reference in the constraint. For our example:

template<typename C>
// Sutton prefers std::common_type_t<C>,
// effectively the same as std::decay_t<C>
requires<Container<std::remove_reference_t<C>>>
void example(C&& c);


Related Topics



Leave a reply



Submit