Concise Explanation of Reference Collapsing Rules Requested: (1) A& & -≫ A& , (2) A& && -≫ A& , (3) A&& & -≫ A& , and (4) A&& && -≫ A&&

Why do we need reference collapsing rules [duplicate]

My question is why do we need these rules if T is already deduced to int&&?

This is not quite true. The rules for type deduction won't deduce the argument to be a reference. That is, in:

template <typename T>
void f(T);

And the expressions:

X g();
X& h();
X a;
f(g()); // argument is an rvalue, cannot be bound by lvalue-ref
f(h()); // argument is an lvalue
f(a); // argument is an lvalue

The deduced type will be X in last two cases and it will fail to compile in the first. The type deduced will be the value type, not a reference type.

The next step is to figure out what the deduced type would be if the template took the argument by lvalue or rvalue reference. In the case of lvalue references, the options are clear, with a modified f:

template <typename T>
void f(T &);

f(g()); // only const& can bind an rvalue: f(const X&), T == const int
f(h()); // f(X&)
f(a); // f(X&)

Up to here it was already defined in the previous version of the standard. Now the question is what should the deduced types be if the template takes an rvalue-references. This is what was added in C++11. Consider now:

template <typename T>
void f(T &&);

And rvalue will only bind to an rvalue, and never to an lvalue. This would imply that using the same simple rules as for lvalue-references (what type T would make the call compile) the second and third calls would not compile:

f(g());     // Fine, and rvalue-reference binds the rvalue
f(h()); // an rvalue-reference cannot bind an lvalue!
f(a); // an rvalue-reference cannot bind an lvalue!

Without the reference collapsing rules, the user would have to provide two overloads for the template, one that takes an rvalue-reference, another that takes an lvalue-reference. The problem is that as the number of arguments increases the number of alternatives grows exponentially, and implementing perfect forwarding becomes almost as hard in C++03 (with the only advantage of being able to detect an rvalue with an rvalue-reference).

So something different needs to be done, and that is reference collapsing, which are really a way of describing the desired semantics. A different way of describing them is that when you type && by a template argument you don't really ask for an rvalue-reference, as that would not allow the call with an lvalue, but you are rather asking the compiler to give you the best type of reference matching.

Concise explanation of reference collapsing rules requested: (1) A& & - A& , (2) A& && - A& , (3) A&& & - A& , and (4) A&& && - A&&

The reference collapsing rules (save for A& & -> A&, which is C++98/03) exist for one reason: to allow perfect forwarding to work.

"Perfect" forwarding means to effectively forward parameters as if the user had called the function directly (minus elision, which is broken by forwarding). There are three kinds of values the user could pass: lvalues, xvalues, and prvalues, and there are three ways that the receiving location can take a value: by value, by (possibly const) lvalue reference, and by (possibly const) rvalue reference.

Consider this function:

template<class T>
void Fwd(T &&v) { Call(std::forward<T>(v)); }

By value

If Call takes its parameter by value, then a copy/move must happen into that parameter. Which one depends on what the incoming value is. If the incoming value is an lvalue, then it must copy the lvalue. If the incoming value is an rvalue (which collectively are xvalues and prvalues), then it must move from it.

If you call Fwd with an lvalue, C++'s type-deduction rules mean that T will be deduced as Type&, where Type is the type of the lvalue. Obviously if the lvalue is const, it will be deduced as const Type&. The reference collapsing rules mean that Type & && becomes Type & for v, an lvalue reference. Which is exactly what we need to call Call. Calling it with an lvalue reference will force a copy, exactly as if we had called it directly.

If you call Fwd with an rvalue (ie: a Type temporary expression or certain Type&& expressions), then T will be deduced as Type. The reference collapsing rules give us Type &&, which provokes a move/copy, which is almost exactly as if we had called it directly (minus elision).

By lvalue reference

If Call takes its value by lvalue reference, then it should only be callable when the user uses lvalue parameters. If it's a const-lvalue reference, then it can be callable by anything (lvalue, xvalue, prvalue).

If you call Fwd with an lvalue, we again get Type& as the type of v. This will bind to a non-const lvalue reference. If we call it with a const lvalue, we get const Type&, which will only bind to a const lvalue reference argument in Call.

If you call Fwd with an xvalue, we again get Type&& as the type of v. This will not allow you to call a function that takes a non-const lvalue, as an xvalue cannot bind to a non-const lvalue reference. It can bind to a const lvalue reference, so if Call used a const&, we could call Fwd with an xvalue.

If you call Fwd with a prvalue, we again get Type&&, so everything works as before. You cannot pass a temporary to a function that takes a non-const lvalue, so our forwarding function will likewise choke in the attempt to do so.

By rvalue reference

If Call takes its value by rvalue reference, then it should only be callable when the user uses xvalue or rvalue parameters.

If you call Fwd with an lvalue, we get Type&. This will not bind to an rvalue reference parameter, so a compile error results. A const Type& also won't bind to an rvalue reference parameter, so it still fails. And this is exactly what would happen if we called Call directly with an lvalue.

If you call Fwd with an xvalue, we get Type&&, which works (cv-qualification still matters of course).

The same goes for using a prvalue.

std::forward

std::forward itself uses reference collapsing rules in a similar way, so as to pass incoming rvalue references as xvalues (function return values that are Type&& are xvalues) and incoming lvalue references as lvalues (returning Type&).

Cast to reference type

Is conversion to a reference even possible?

Yes, depending on the object you begin with.
Most simple example is one lvalue reference converted to another. This can even be an added cv-qualifier (or removed, with const_cast), such as:

int const & foo(int & i) {
return static_cast<int const &>(i);
}

the derived-to-base casts and base-to-derived (dynamic) casts mentioned in the other answers are other such examples.

A more interesting example is the std::reference_wrapper, which is an object you can receive as an rvalue, and cast it to an lvalue reference of the contained type:

int & foo(std::reference_wrapper<int> i) {
return static_cast<int &>(i);
}

Note that the cast above happens implicitly (I could've used return i), but in some contexts (e.g. capturing variables with auto type) you might want to write the cast explicitly.

What is meant in C++ Primer is simply that the behavior of casts in these examples, and others, is basically what you would expect - the result of a cast to a reference type is an lvalue.

why not introduce autoval/autoref or decltype_val/decltype_ref to C++?

auto behaves like function template argument deduction : it strips away const, volatile and references, and uses reference collapsing rules. Thus :

  • auto i = x is always a value;
  • auto &i = x is always a lvalue reference;
  • auto &&i = x is always either an lvalue or a rvalue reference, depending on x.

decltype has no deduction or type adjustment whatsoever : it is always the type of its operand, const and references and all. It does have the "gotcha" that :

  • decltype(x) yields the exact type of x, whereas
  • decltype((x)) yields the type of the expression (x), which forms a reference to x.

C++ why does passing an lvalue to a move constructor work for templates?

Yes. In the case of a template function, the compiler deduces the template argument T such that it matches the argument given.

Since someData is in fact an lvalue, T is deduced as SomeData &. The declaration of Function, after type deduction, then becomes

void Function(SomeData & &&)

and SomeData & &&, following the rules for reference collapsing, becomes SomeData &.

Hence, the function argument someData becomes an lvalue-reference and is passed as such to the initialization of localData. Note that (as @aschepler pointed out correctly) localData is declared as T, so it is itself a reference of type SomeData &. Hence, no copy construction happens here – just the initialization of a reference.


If you want localData to be an actual copy, you would have to turn the type from SomeData & into SomeData, i.e. you would have to remove the & from the type. You could do this using std::remove_reference:

template<class T>
void Function(T &&someData)
{
/* Create a copy, rather than initialize a reference: */
typename std::remove_reference<T>::type localData(someData);
localData.Print();
}

(For this, you'd have to #include <type_traits>.)

What is the type of a reference of a reference in a template class [duplicate]

T is const int& because that's what you told it to be.

T& is also const int& because reference collapsing transforms your T& into T:

[dcl.ref]/6: If a typedef-name ([dcl.typedef], [temp.param]) or a decltype-specifier ([dcl.type.simple]) denotes a type TR that is a reference to a type T, an attempt to create the type “lvalue reference to cv TR” creates the type “lvalue reference to T”, while an attempt to create the type “rvalue reference to cv TR” creates the type TR. [ Note: This rule is known as reference collapsing. — end note ] [ Example:

int i;
typedef int& LRI;
typedef int&& RRI;

LRI& r1 = i; // r1 has the type int&
const LRI& r2 = i; // r2 has the type int&
const LRI&& r3 = i; // r3 has the type int&

RRI& r4 = i; // r4 has the type int&
RRI&& r5 = 5; // r5 has the type int&&

decltype(r2)& r6 = i; // r6 has the type int&
decltype(r2)&& r7 = i; // r7 has the type int&

— end example ]

This is for convenience, because there is no such thing as const int& & (reference to reference; not to be confused with rvalue reference type const int&& which does exist!) and it's handy to be able to just write code like yours without having to manually "get rid of" the "extra" &.

The rationale behind this rule is explained in more detail here:

  • Concise explanation of reference collapsing rules requested: (1) A& & -> A& , (2) A& && -> A& , (3) A&& & -> A& , and (4) A&& && -> A&&

Confusion about the return type of std::get() on std::tuple objects

Look at signature of std::get:

template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >&
get( tuple<Types...>& t )

template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >&&
get( tuple<Types...>&& t )

In your case t is a l-value, so it return int&& & which became int&.

Is it specified in the C++11 standard that std::begin(Container&&) returns const_iterator?

Let's try to analyze what happens, step by step:

  1. You're calling std::begin(std::vector<int>&&), but std::begin has no overload that takes an rvalue:

    template< class C > 
    auto begin( C& c ) -> decltype(c.begin());

    template< class C >
    auto begin( const C& c ) -> decltype(c.begin());


  1. Due to reference collapsing, a temporary (xvalue) will only bind to a const lvalue reference:

    If you call Fwd with an xvalue, we again get Type&& as the type of v. This will not allow you to call a function that takes a non-const lvalue, as an xvalue cannot bind to a non-const lvalue reference. It can bind to a const lvalue reference, so if Call used a const&, we could call Fwd with an xvalue.

    (From the linked answer).



  1. Therefore, the

     template<class C> auto begin(const C& c) -> decltype(c.begin());

    overload is being called, which returns a const iterator.

    Why?

    Because std::begin(v) calls v.begin(), which returns a const_iterator when called on const instances of std::vector.



Related Topics



Leave a reply



Submit