c++templatesrvaluelvaluereference-collapsing

What are the rules of rvalue(&&) lvalue(&) reference binding in templates with regard to reference collapsing?


Consider these cases:

int i{};
int& ir{i};

class A{
public:
    int& i;
    A(int&& pi):i(pi){}
};

A a1{i}; // Error // case 1
A a2{int(1)}; // OK // case 2

class B{
public:
    int& i;
    template<typename TYPE>
    B(TYPE&& pi):i(pi){}
};

B b1{i}; // OK // case 3
B b2{int(1)}; //OK // case 4
int& ii{int(12)}; // Error // case 5
int& iii{std::move(int(12))}; // Error // case 6

template<typename TYPE>
class C{
public:
    TYPE& i;
    C(TYPE&& pi):i(pi){}
};

C c1{i}; // Error // case 7
C c2{int(1)}; // OK // case 8
C<int&> c3{i}; // OK // case 9
C<int&> c4{int(1)}; // Error // case 10
int&& iiii{ir}; // Error // case 11

A rvalue can not be bound to lvalue and if my understanding is correct, TYPE&& would either collapse to TYPE&& or TYPE&. How ever I am having a hard time understanding these cases, specially case 4. If case 4 is correct, then it means we have b2.i which is a reference, initialized from a temporary (rvalue). Then why case 2, 5, 6 and 7 are incorrect? And when case 9 is correct it mean the TYPE&& is int&&& which (I assume) collapses to int& , then how could c3.i which is an rvalue(int&&) be initialized from a lvalue, while case 10 and 11 are incorrect?

I wish some one could explain the general rules regarding this subject and also these cases in detail.


Solution

  • There are several mechanisms at play here: lvalue/rvalue semantics, reference collapsing, forwarding reference and CTAD. I think explaining the cases you listed should be sufficient to form a big picture.

    1. rvalue reference int&& pi can't be bound to lvalue i.
    2. rvalue reference int&& pi can be bound to rvalue int(1). pi in i(pi) refers to a memory location so it is an lvalue, allowing a2.i to be initialized. After construction a2.i is a dangling reference to int(1).
    3. forwarding reference TYPE&& pi can be bound to lvalue i. TYPE is int&. b1.i refers to i.
    4. forwarding reference TYPE&& pi can be bound to rvalue int(1). TYPE is int. TYPE&& pi is an rvalue reference, similar to case 2 b2.i is a dangling reference to int(1).
    5. lvalue reference int& ii can not be bound to rvalue int(12).
    6. lvalue reference int& iii can not be bound to rvalue std::move(int(12)).

    In cases 7-10 TYPE&& pi is not a forwarding reference because TYPE is a template parameter of class C, not of constructor. CTAD, used in 7-8 to deduce TYPE, doesn't change that: an rvalue is expected, only then TYPE can be deduced to int, forming int&& pi.

    1. TYPE can't be deduced.
    2. TYPE is deduced to be int. c2.i, declared as int& i;, forms a dangling reference to int(1).
    3. TYPE is explicitly int&. TYPE&& pi is collapsed to int& pi. TYPE& i is collapsed to int& i. c3.i refers to i.
    4. TYPE&& pi is collapsed to int& pi, lvalue reference can't be bound to int(1).
    5. rvalue reference int&& iiii can't be bound to lvalue ir.

    And a bonus case that might make cases 2 and 4 easier to understand:

    int&& rvr = 1;
    int&& rvr2 = rvr;
    

    The first line is correct, the last is an error. int&& rvr is an rvalue reference and extends lifetime of a temporary. But rvr mentioned in the last line is an lvalue, so rvalue reference int&& rvr2 can't be bound to it.