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?
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:
The first first bullet point covers any list-initialization. This jumps
X x{y}
andX x = {y}
over to [dcl.init.list]. We'll get back to that. The other case is easier. Let's look atX x = y
. We call straight down into:[dcl.init]/17.6.3:
The candidates in [over.match.copy] are:
This gives us candidates:
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 ofX(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:
which in both cases takes us to [over.match.list] which defines two-phase overload resolution:
The candidates are the constructors of
X
. The only difference betweenX x{y}
andX x = {y}
are that if the latter chooses anexplicit
constructor, the initialization is ill-formed. We don't even have anyexplicit
constructors, so the two are equivalent. Hence, we enumerate our constructors:X(Y const& )
X(X&& )
by way ofY::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.