List initialization of a reference: is GCC or Clang correct?

  • A+
Category:Languages

Given this example:

int g_i = 10; struct S {     operator int&(){ return g_i; } };  int main() {     S s;     int& iref1 = s; // implicit conversion      int& iref2 = {s}; // clang++ error, g++ compiles fine:                       // `s` is converted                       // to a temporary int and binds with                       // lvalue reference      int&& iref3 = {s}; // clang++ compiles, g++ error:                        // cannot bind rvalue reference                        // to lvalue } 

The errors are as described in the comments.
gcc 8.2.1 and clang 7.0.1 were used and disagree about what is happening in this example. Could someone clarify this?

In list initialization :

Otherwise, if the initializer list has a single element of type E and either T is not a reference type or its referenced type is reference-related to E, the object or reference is initialized from that element (by copy-initialization for copy-list-initialization, or by direct-initialization for direct-list-initialization); if a narrowing conversion (see below) is required to convert the element to T, the program is ill-formed.

Otherwise, if T is a reference type, a prvalue of the type referenced by T is generated. The prvalue initializes its result object by copy-list-initialization or direct-list-initialization, depending on the kind of initialization for the reference. The prvalue is then used to direct-initialize the reference. [ Note: As usual, the binding will fail and the program is ill-formed if the reference type is an lvalue reference to a non-const type. — end note ]

In reference initialization:

Given types “cv1 T1” and “cv2 T2”, “cv1 T1” is reference-related to “cv2 T2” if T1 is the same type as T2, or T1 is a base class of T2. “cv1 T1” is reference-compatible with “cv2 T2” if
- T1 is reference-related to T2, or
- T2 is “noexcept function” and T1 is “function”, where the function types are otherwise the same,

...and later on there's some (personally ambiguous) language on user-defined conversions:

For example:

If the reference is an lvalue reference and the initializer expression
...
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” (this conversion is selected by enumerating the applicable conversion functions ([over.match.ref]) and choosing the best one through overload resolution),
...
then the reference is bound to the ... value result of the conversion

...

Otherwise, if the initializer expression
...
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 rvalue or function lvalue of type “cv3 T3”, where “cv1 T1” is reference-compatible with “cv3 T3”
... then the value of the ... result of the conversion in the second case is called the converted initializer. If the converted initializer is a prvalue, its type T4 is adjusted to type “cv1 T4”

...

Otherwise:
- If T1 or T2 is a class type and T1 is not reference-related to T2, user-defined conversions are considered using the rules for copy-initialization of an object of type “cv1 T1” by user-defined conversion ... The result of the call to the conversion function, as described for the non-reference copy-initialization, is then used to direct-initialize the reference. For this direct-initialization, user-defined conversions are not considered.

...

Otherwise, the initializer expression is implicitly converted to a prvalue of type “cv1 T1”. The temporary materialization conversion is applied and the reference is bound to the result.

These rules are quite nuanced and I cannot fully grasp each situation. To me, it seems like a prvalue should be getting generated (I agree with clang), but the language on reference initialization, and interaction with list initialization is very fuzzy.

 


Let's read the standard in the correct order, so that we know which sections apply to the situation at hand.

[dcl.init]/17 says:

The semantics of initializers are as follows... If the initializer is a (non-parenthesized) braced-init-list or is = braced-init-list, the object or reference is list-initialized (11.6.4) ...

So we go to [dcl.init.list] (11.6.4). Paragraph 3 says:

List-initialization of an object or reference of type T is defined as follows: (... cases that don't apply are elided from this quotation...) Otherwise, if the initializer list has a single element of type E and either T is not a reference type or its referenced type is reference-related to E ... otherwise, if T is a reference type, a prvalue of the type referenced by T is generated. The prvalue initializes its result object by copy-list-initialization or direct-list-initialization, depending on the kind of initialization for the reference. The prvalue is then used to direct-initialize the reference. [ Note: As usual, the binding will fail and the program is ill-formed if the reference type is an lvalue reference to a non-const type. —end note ]

According to [dcl.init.ref]/4:

Given types “cv1 T1” and “cv2 T2”, “cv1 T1” is reference-related to “cv2 T2” if T1 is the same type as T2, or T1 is a base class of T2.

Therefore, in your code, the referenced type int is not reference-related to the type in the initializer list, namely S. Thus, by [dcl.init.list]/3, a prvalue of type int is generated, and it takes the form int{s}. And as the note says, in the case of iref2, the program is ill-formed because it tries to bind a non-const lvalue reference to a prvalue. In the case of iref3, the program should compile since iref3 is being bound to the prvalue result int{s}.

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: