Is There a Difference Between Universal References and Forwarding References

Is there a difference between universal references and forwarding references?


Do they mean the same thing?

Universal reference was a term Scott Meyers coined to describe the concept of taking an rvalue reference to a cv-unqualified template parameter, which can then be deduced as either a value or an lvalue reference.

At the time the C++ standard didn't have a special term for this, which was an oversight in C++11 and makes it hard to teach. This oversight was remedied by N4164, which added the following definition to [temp.deduct]:

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.

Hence, the two mean the same thing, and the current C++ standard term is forwarding reference. The paper itself articulates why "forwarding reference" is a better term than "universal reference."

Is it only a forwarding reference if the function body calls std::forward?

Nope, what you do with a forwarding reference is irrelevant to the name. The concept forwarding reference simply refers to how the type T is deduced in:

template <class T> void foo(T&& ); // <== 

It does not need to be subsequently forwarded .

Constant forwarding reference vs constant reference for forwarding references


What is the difference between const auto&& and const auto& in regard to universal references?

Their difference is that const auto&& is an rvalue reference to const and const auto& is an lvalue reference to const. What they have in common is that neither is a forwarding reference. Only auto&& is a forwarding reference (or T&& where T is deduced).

const auto&& does not bind to lvalues, unlike const auto& and T&&.

Why the difference in the flow of universal reference and rvalue reference

In case 1, T is deduced to be const char (&)[12], not std::string. There is no reason for the compiler to promote the string literal to std::string yet. In case 2, every overload requires takes a reference to an std::string, which forces the creation of a temporary std::string to which a reference can be bound using the implicit const char* constructor.

Note that while an rvalue reference such as std::string && may only bind to an rvalue, the templated equivalent T && may bind to both rvalues and lvalues.

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.

rvalue reference or forwarding reference?

It's an rvalue reference. Forwarding references can only appear in a deduced context. This is just a member function that accepts an rvalue reference to the class template parameter.

You can't force a forwarding reference to be an rvalue reference if you want to maintain template argument deduction for functions. If you don't mind specifying the template argument all over the place, then this will always and only ever give an rvalue reference:

template<typename T> struct identity { using type = T; };

template<typename T> void func(typename identity<T>::type&&);

In retrospect, there actually is a way to maintain deduction but force only rvalue refs to be accepted (besides the self documenting one in Simple's answer). You can provide a deleted lvalue overload:

template<typename T>
void func(T&) = delete;

template<typename T>
void func(T&& s)
{
// ...
}

The lvalue overload is more specialized when passed an lvalue. And on account of being deleted, will give a somewhat clear error message.

Why is const template parameter not a universal/forwarding reference

Foreword: The official term is forwarding reference.

So why is the second case not a universal reference?

Because it is a reference to const. And references to const are not forwarding references.

why?

The whole point of a forwarding reference is that when an rvalue is given as an argument, the parameter will be deduced as reference to non-const rvalue which allows such argument to be moved from when forwarding (while simultaneously allowing lvalues to not be moved from). You cannot move from a reference to const because the argument of the move constructor will be an rvalue reference to non-const which cannot be bound to a reference to const.

The language-lawyer answer is: Because the standard says so:

[temp.deduct.call] A forwarding reference is an rvalue reference to a cv-unqualified template parameter that does not represent a template parameter of a class template ...



Related Topics



Leave a reply



Submit