The following behavior is rather strange to me:
class Y
{
public:
Y(int) { cout << "Y\n"; }
};
class X
{
public:
X(int, const Y&) { cout << "int, const Y&\n"; }
explicit X(int) { cout << "X\n"; }
X(int, X&&) { cout << "int, X&&\n"; }
X(const X&) { cout << "copy\n"; }
X(int, int, const Y&) { cout << "int, int, const Y&\n"; }
explicit X(int, int, int) { cout << "3 ints\n"; }
};
int main()
{
X x1(1, 2); // OK
X x2({ 1,2,3 }); // error, OK if X(int, int, int) becomes, say, X(int, int, X&&)
}
In my understanding, X x1(1, 2);
is OK because when the compiler tries to make an X&&
from 2
, it finds that X(int)
is explicit
, hence the compiler treats X(int, const Y&)
as a better match (had X(int)
not been explicit
, X(int, X&&)
would be the better match). On the other hand, X x2({ 1,2,3 });
is not OK because while X(int, int, int)
is treated as the better match (since there is not restriction for initializing the 3 int
s by 1
,2
and 3
respectively), it is explicit
and thus cannot be called implicitly.
Why don't the compiler just check the explicit
ness on X(int, int, int)
and "take a step back" to prefer X(int, int, const Y&)
as the better match?
The answers mentioned that ambiguity will arise if X(int)
is not explicit
. In fact, I find that for msvc C 20/17/14 there will not be ambiguity error, while gcc and clang will report the error... why...?
CodePudding user response:
(had X(int) not been explicit, X(int, X&&) would be the better match)
No, in that case the overload resolution would have been ambiguous, because there is no ordering between different user-defined conversions. Even if one will in the end up binding a rvalue reference and the other a lvalue reference, neither will be considered better than the other.
Why don't the compiler just check the explicitness on X(int, int, int) and "take a step back" to prefer X(int, int, const Y&) as the better match?
The only possible constructor to use for the initialization
X x2({ 1,2,3 });
is the copy constructor of X
. All other constructors expect either multiple arguments (but there is only one here) or expect a single int
, which cannot be initialized from a three-element brace-enclosed initializer list.
So, the question is how to get from {1,2,3}
to const X&
in the copy constructor's parameter. Since you don't have any std::initializer_list
constructors (which would be preferred), this will simply be tested by doing overload resolution again against the constructors of X
with three arguments.
The rules for this are a bit different than normally for copy-initialization. Instead of ignoring explicit
constructors, in the case of initializer list arguments they are considered, but if overload resolution ends up choosing one, then the initialization is considered ill-formed. This special behavior for initializer list arguments is specified in [over.match.list]/1.
That's why the compiler doesn't "back-track" or ignore the explicit
constructor, although it can't be used.
See this question for discussion on the motivation behind this rule for initializer list arguments. There is also CWG issue 1228 arguing that this is counter-intuitive. It was closed as "not a defect", but unfortunately only with a remark that the behavior is intended.
CodePudding user response:
Lets see on case by case basis what is happening and why in the first case(X x1(1, 2);
) the program works but in 2nd case (X x2({ 1,2,3 });
) we get ambiguity error.
Case 1
Here we consider:
X x1(1, 2);
The explanation given for this case in your question isn't correct. In particular, you said that if we remove the explicit
from the X::X(int)
ctor then X::X(int, X&&)
would be the better match, which is not true. You can confirm this here. As you will see in the above linked demo, if we removed the explicit
from X::X(int)
ctor then we will get an ambiguity error between the X::X(int, X&&)
and X::X(int, const Y&)
ctors. Note msvc has a bug as it compiles the program when X::X(int)
is not explicit.
This means that both X::X(int, X&&)
and X::X(int, const Y&)
have the same rank and so the compiler cannot decide which one to choose.
Thus, when you made X::X(int)
an explicit
ctor you decided for the compiler that the X(int, const Y&)
ctor should be used because now the other ctor X::X(int, X&&)
would use an user defined explicit
ctor . Thus only the X(int, const Y&)
remains viable.
Case 2
Here we consider:
X x2({ 1,2,3 });
The reason this case fails is because here the X::X(int, int, int)
is a better match than the X::X(int, int, const Y&)
ctor. That is, here the compiler has already decided that the X::X(int, int, int)
should be used. But because this ctor is explicit
we get the error saying:
converting to ‘const X’ from initializer list would use explicit constructor ‘X::X(int, int, int)’
Summary
Case 1 doesn't fail because there the compier hasn't decided which of the two X::X(int, X&&)
and X::X(int, const Y&)
equally ranked ctors to choose. It can't because they are equally ranked. But by making X::X(int)
explicit
you have made the decision that the X::X(int, const Y&)
should be choosen over X::X(int, X&&)
.
While Case 2 fails because there the compiler has decided that the X::X(int, int, int)
is a better match than the X::X(int, int, const Y&)
. But since X::X(int, int, int)
is explcit
this fails with the mentioned error.
I find that for msvc C 20/17/14 there will not be ambiguity error, while gcc and clang will report the error... why...?
It seems to be bug in msvc.