Given the following:
#include <stdio.h>
class X;
class Y
{
public:
Y() { printf(" 1\n"); } // 1
// operator X(); // 2
};
class X
{
public:
X(int) {}
X(const Y& rhs) { printf(" 3\n"); } // 3
X(Y&& rhs) { printf(" 4\n"); } // 4
};
// Y::operator X() { printf(" operator X() - 2\n"); return X{2}; }
int main()
{
Y y{}; // Calls (1)
printf("j\n");
X j{y}; // Calls (3)
printf("k\n");
X k = {y}; // Calls (3)
printf("m\n");
X m = y; // Calls (3)
printf("n\n");
X n(y); // Calls (3)
return 0;
}
So far, so good. Now, if I enable the conversion operator Y::operator X()
, I get this;-
X m = y; // Calls (2)
My understanding is that this happens because (2) is 'less const' than (3) and
therefore preferred. The call to the X
constructor is elided
My question is, why doesn't the definition X k = {y}
change its behavior in the same way? I know that = {}
is technically 'list copy initialization', but in the absence of a constructor taking an initializer_list
type, doesn't this revert to 'copy initialization' behavior? ie - the same as for X m = y
Where is the hole in my understanding?
The Copy initialization can be done using the concept of copy constructor. As we know that the constructors are used to initialize the objects. We can create our copy constructor to make a copy of some other object, or in other words, initialize current object with the value of another object.
In other words, a good compiler will not create a copy for copy-initialization when it can be avoided; instead it will just call the constructor directly -- ie, just like for direct-initialization.
Copy Constructor in C++ClassName (const ClassName &old_obj); Copy constructor is used to initialize the members of a newly created object by copying the members of an already existing object. Copy constructor takes a reference to an object of the same class as an argument.
Initializer List is used in initializing the data members of a class. The list of members to be initialized is indicated with constructor as a comma-separated list followed by a colon. Following is an example that uses the initializer list to initialize x and y of Point class.
Where is the hole in my understanding?
tltldr; Nobody understands initialization.
tldr; List-initialization prefers std::initializer_list<T>
constructors, but it doesn't fall-back to non-list-initialization. It only falls back to considering constructors. Non-list-initialization will consider conversion functions, but the fallback does not.
All of the initialization rules come from [dcl.init]. So let's just go from first principles.
[dcl.init]/17.1:
- If the initializer is a (non-parenthesized) braced-init-list or is = braced-init-list, the object or reference is list-initialized.
The first first bullet point covers any list-initialization. This jumps X x{y}
and X x = {y}
over to [dcl.init.list]. We'll get back to that. The other case is easier. Let's look at X x = y
. We call straight down into:
[dcl.init]/17.6.3:
- Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type or (when a conversion function is used) to a derived class thereof are enumerated as described in [over.match.copy], and the best one is chosen through overload resolution.
The candidates in [over.match.copy] are:
- The converting constructors of
T
[in our case,X
] are candidate functions.- When the type of the initializer expression is a class type “cv
S
”, the non-explicit conversion functions ofS
and its base classes are considered.In both cases, the argument list has one argument, which is the initializer expression.
This gives us candidates:
X(Y const &); // from the 1st bullet
Y::operator X(); // from the 2nd bullet
The 2nd is equivalent to having had a X(Y& )
, since the conversion function is not cv-qualified. This makes for a less cv-qualified reference than the converting constructor, so it's preferred. Note, there is no invocation of X(X&& )
here in C++17.
Now let's go back to the list-initialization cases. The first relevant bullet point is [dcl.init.list]/3.6:
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.
which in both cases takes us to [over.match.list] which defines two-phase overload resolution:
- Initially, the candidate functions are the initializer-list constructors ([dcl.init.list]) of the class T and the argument list consists of the initializer list as a single argument.
- 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.
If the initializer list has no elements and T has a default constructor, the first phase is omitted. In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.
The candidates are the constructors of X
. The only difference between X x{y}
and X x = {y}
are that if the latter chooses an explicit
constructor, the initialization is ill-formed. We don't even have any explicit
constructors, so the two are equivalent. Hence, we enumerate our constructors:
X(Y const& )
X(X&& )
by way of Y::operator X()
The former is a direct reference binding that is an Exact Match. The latter requires a user-defined conversion. Hence, we prefer X(Y const& )
in this case.
Note that gcc 7.1 gets this wrong in C++1z mode, so I've filed bug 80943.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With