c++initializationlanguage-lawyerlist-initializationreference-initialization

Reference initialization using list initialization accepted by gcc and msvc but rejected by clang


Consider the following example that compiles with gcc, msvc and edg but is rejected by clang. Demo

struct X
{
    public:
    X(){}
};
struct Y : X 
{
    
};
struct Z
{
    public:
        operator const Y () const
        {
            return {};
        }
};
int main()
{
    Z z;
    X&& r{z}; //Clang: Nope, gcc: Ok, msvc: Ok, Edg: Ok
}

Only clang reject the above program saying:

<source>:22:11: error: no viable conversion from 'Z' to 'X'
   22 |     X&& r{z}; //Clang: Nope, gcc: Ok, msvc: Ok, Edg: Ok
      |           ^
<source>:2:8: note: candidate constructor (the implicit copy constructor) not viable: no known conversion from 'Z' to 'const X &' for 1st argument
    2 | struct X
      |        ^
<source>:2:8: note: candidate constructor (the implicit move constructor) not viable: no known conversion from 'Z' to 'X &&' for 1st argument
    2 | struct X
      |        ^
<source>:14:9: note: candidate function
   14 |         operator const Y () const
      |         ^
<source>:2:8: note: passing argument to parameter here
    2 | struct X
      |        ^

What is the correct behavior here as per the latest c++ standard.

Note that I am not looking for a workaround but to understand what is the correct behavior as per the standard here.


Solution

  • The program is well-formed as explained below.

    First X&& r{z}; is list-initialization. So we reach dcl.init.list#3:

    List-initialization of an object or reference of type cv T is defined as follows:

    • [..]
    • [...]
    • Otherwise, if T is a reference type, a prvalue is generated. The prvalue initializes its result object by copy-list-initialization from the initializer list. The prvalue is then used to direct-initialize the reference. The type of the prvalue is the type referenced by T, unless T is “reference to array of unknown bound of U”, in which case the type of the prvalue is the type of x in the declaration U x[] H, where H is the initializer list.

    So first we need to check if the copy-list-initialization of the result object from the initializer list {z}. Basically, we need to check the validity of X resultObject = {z}; to move further.

    So we go to dcl.init#general-16:

    The semantics of initializers are as follows. The destination type is the cv-unqualified type of the object or reference being initialized and the source type is the type of the initializer expression. If the initializer is not a single (possibly parenthesized) expression, the source type is not defined.

    • If the initializer is a (non-parenthesized) braced-init-list or is = braced-init-list, the object or reference is list-initialized ([dcl.init.list]).

    So we again move to dcl.init.list-3.7, this time to check the validity of X resultObject = {z};:

    List-initialization of an object or reference of type cv T is defined as follows:

    • [...]
    • [...]
    • Otherwise, if T is a class type, constructors are considered. The applicable constructors are enumerated and the best one is chosen through overload resolution ([over.match], [over.match.list]). If a narrowing conversion (see below) is required to convert any of the arguments, the program is ill-formed.

    Now the above along with over.match.list#-1.2 means that X's constructors(including the implicit copy ctor etc) will be enumerated for the argument z.

    Otherwise, or if no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

    Now we move to over.match:

    Overload resolution selects the function to call in seven distinct contexts within the language:

    • [...]
    • [...]
    • invocation of a user-defined conversion for copy-initialization of a class object ([over.match.copy]);

    Now, we go to over.match#copy:

    Under the conditions specified in [dcl.init], as part of a copy-initialization of an object of class type, a user-defined conversion can be invoked to convert an initializer expression to the type of the object being initialized. Overload resolution is used to select the user-defined conversion to be invoked.

    Assuming that “cv1 T” is the type of the object being initialized, with T a class type, the candidate functions are selected as follows:

    • [...]
    • When the type of the initializer expression is a class type “cv S”, conversion functions are considered. The permissible types for non-explicit conversion functions are T and any class derived from T. When initializing a temporary object ([class.mem]) to be bound to the first parameter of a constructor where the parameter is of type “reference to cv2 T” and the constructor is called with a single argument in the context of direct-initialization of an object of type “cv3 T”, the permissible types for explicit conversion functions are the same; otherwise there are none.

    In both cases, the argument list has one argument, which is the initializer expression.

    This means that the conversion function Y::operator const Y () const can be used to convert the initializer expression z to an rvalue which will then be used as argument for one of X's ctors.

    The important thing is that only the implicit copy ctor X::X(const X&&) is viable here for the rvalue obtained as a resut of the conversion function.The implicit move ctor X::X(X&&) is not viable because it's parameter is a non-const rvalue reference.


    Note that if you change the move ctor's parameter to be const X&&(demo), then both the copy ctor and the move ctor will be viable for the rvalue obtaied from the conversion operator but then move ctor X::X(const X&&) will be a better match than the copy ctor X::X(const X&) because the argument(result of conversion operator) is a rvalue.