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.
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.