4
votes

I've written the following example:

#include <iostream>

volatile int&& bar()
{
    return 1;
}

int main()
{
    const int& i = bar(); //error: binding of reference to type 'const int' 
                          //to a value of type 'volatile int' drops qualifiers
}

DEMO

But if we replace int&& with int it works fine:

#include <iostream>

volatile int bar()
{
    return 1;
}

int main()
{
    const int& i = bar(); //OK

}

DEMO

that's not exactly clear. What the Standard says is (8.5.3/5 [dcl.init.ref]):

A reference to type “cv1 T1” is initialized by an expression of type “cv2 T2” as follows:

— If the reference is an lvalue reference and the initializer expression

  • is an lvalue (but is not a bit-field), and “cv1 T1” is reference-compatible with “cv2 T2,” or

  • has a class type (i.e., T2 is a class type), where T1 is not reference-related to T2, and can be converted to an lvalue of type “cv3 T3,” where “cv1 T1” is reference-compatible with “cv3 T3” 108 (this conversion is selected by enumerating the applicable conversion functions (13.3.1.6) and choosing the best one through overload resolution (13.3)), then the reference is bound to the initializer expression lvalue in the first case and to the lvalue result of the conversion in the second case (or, in either case, to the appropriate base class subobject of the object).

[...]

Otherwise, the reference shall be an lvalue reference to a non-volatile const type (i.e., cv1 shall be const), or the reference shall be an rvalue reference.

Well, in the first example we have an rvalue of type volatile int&&. And the 'otherwise' case is applicable to both two examples. But 5/5 [expr] says:

If an expression initially has the type “reference to T” (8.3.2, 8.5.3), the type is adjusted to T prior to any further analysis

So, essentially we have an rvalue of type volatile int instead of volatile int&&, which means both these two examples shall work in the same way.

1

1 Answers

3
votes

The difference between the two cases is that in Case 1, the references binds directly and in Case 2, it does not bind directly (i.e. the reference binds to a temporary; definition can be found in the last paragraph of [dcl.init.ref]).

The direct binding fails because T2 is volatile-qualified and T1 isn't (in the Standard terminology, T1 is not reference-compatible with T2).

The indirect binding succeeds because when a temporary int is initialized from the reference returned by bar(), the temporary is not volatile. (The temporary has type cv1 T1).


To see why Case 1 is a direct binding. Firstly, bar() is an xvalue here. See [basic.lval]/1 "The result of calling a function whose return type is an rvalue reference is an xvalue".

From [dcl.init.ref]/5:

  • If the reference is an lvalue reference and the initializer expression [is an lvalue] or [has class type]

Not applicable: the initializer is an xvalue (not an lvalue), and it is a reference so it does not have class type.

  • Otherwise, the reference shall be an lvalue reference to a non-volatile const type (i.e., cv1 shall be const), or the reference shall be an rvalue reference

This does apply: const int & is an lvalue reference to non-volatile const type. Going down this tree:

If the initializer expression

  • is an xvalue (but not a bit-field), class prvalue, array prvalue or function lvalue and “cv1 T1” is reference-compatible with “cv2 T2”, or
  • [another case]

then the reference is bound to the value of the initializer expression in the first case [...]

This does apply because bar() is an xvalue. So the reference is bound to the xvalue and this is known as direct binding because it is not bound to a temporary.


NB. All Standard references are from C++14 (N3936) . This section changed from C++11 due to DR1288.