1

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 ints by 1,2 and 3 respectively), it is explicit and thus cannot be called implicitly.

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

CPPL
  • 726
  • 1
  • 10
  • 1
    `X x2({ 1,2,3 });` is interpreted as `X x2(X{ 1,2,3 });`: create a temporary using three-ints constructor, then copy-initialize from that temporary (the copy is ultimately elided). This doesn't work because three-ints constructor is explicit. The syntax to call three-ints constructor directly would be `X x2(1,2,3);` or `X x2{1,2,3};` With the choice of syntax you use, the compiler has to look for (1) a one-parameter constructor that (2) can take an object constructible from brace-init list `{1, 2, 3}` – Igor Tandetnik Jun 06 '22 at 04:12
  • 1
    @IgorTandetnik OP's question is why if it is copy-initialization the `explicit` constructor is not simply ignored and the other non-`explicit` one chosen, which is how it works with usual copy-initialization. Also `X x2(X{ 1,2,3 });` is direct-list-initialization followed by direct-initialization (the latter skipped over since C++17), no copy-initialization. – user17732522 Jun 06 '22 at 04:18
  • @CPPL For your edit: MSVC has a [bug](https://godbolt.org/z/dfEr3b7vc). – Jason Jun 06 '22 at 04:47
  • @AnoopRana So... can I say `X&&` is preferred over `const X&` only when object of type `X` is passed directly? (situations like [this](https://godbolt.org/z/n4K1d5rMj)) – CPPL Jun 06 '22 at 05:00
  • @CPPL No, in your given demo the `X&&` version is choosen because `X(2)` is a prvalue. If you were to pass an lvalue, then the `const X&` version will be preffered/choosen. See [Demo](https://onlinegdb.com/1yyA3ageV) – Jason Jun 06 '22 at 05:04
  • @AnoopRana Sorry I actually mean "an rvalue expression representing object of type `X` is passed directly" – CPPL Jun 06 '22 at 05:07
  • @CPPL Yes, as long as it(object of type `X`) is an rvalue `X&&` will be preferred. – Jason Jun 06 '22 at 05:09
  • @AnoopRana Thanks :) but btw, this can be another "strange" implementation for me: why wouldn't just make an rvalue passed, which can be implicitly converted into an `X`, to be preferred by `X&&`? haha... – CPPL Jun 06 '22 at 05:15
  • @CPPL The question in your last comment wasn't clear. In particular, i mean i didn't understand what you're asking when you said: *"why wouldn't just make an rvalue passed, which can be implicitly converted into an `X`, to be preferred by `X&&`?"* . Maybe you can give a demo to clear that up or ask a separate question for that. – Jason Jun 06 '22 at 05:35
  • @AnoopRana I mean if we pass an rvalue of type `X`, then `X&&` is preferred over `const X&` (as the `X(2)` in your demo). Here in the above code, the `2` in `X x1(1,2)` will first need to be used to construct an `X` temporary or an `Y` temporary in order to call `X(int, const Y&)` or `X(int, X&&)`. The current implementation will report ambiguity at this stage. But since `2` in `X x1(1,2)` is an rvalue, I think the compiler could just be implemented to select `X(int, X&&)` rather than giving ambiguity. That would be more consistent with the case where we pass `X(2)`. Just some random thoughts – CPPL Jun 06 '22 at 05:53
  • @CPPL Soppose `X::X(int)` is not explicit. Then the reason it can't happen this time is because now there are two conversion sequences using `X::X(int, X&&)` and `X::X(int, const Y&)` that are **equally ranked**. And so the compiler won't try anything further as it know one can't be chosen over other. But by making `X::X(int)` explicit you've made one of the conversion sequence better than the other. That is they are no more equally ranked. As the one that will use `X::X(int, X&&)` isn't even viable anymore because it will use an explict `X::X(int)` ctor. – Jason Jun 06 '22 at 06:10

2 Answers2

2

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

user17732522
  • 53,019
  • 2
  • 56
  • 105
1

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.

Jason
  • 36,170
  • 5
  • 26
  • 60