2

Consider the following toy code:

class Y
{
public:
    Y(int, int) { cout << "Y ctor\n"; }
};

class X
{
public:
    //X(initializer_list<int>) { cout << "init\n"; }      // 1
    X(Y&&) { cout << "Y&&\n"; }                         // 2
    X(int, int) { cout << "param\n"; }                  // 3
    X(const X&) { cout << "X&\n"; }                     // 4
    X(const Y&) { cout << "Y&\n"; }                     // 5
    X(Y) { cout << "Y\n"; }                             // 6
    X(X&&) { cout << "X&&\n"; }                         // 7
};

void f(const X&) {  }
void f(const Y&) {  }
void f(Y) {  }

int main()
{
    X x({1,2});
    //f({ 1,2 });                                         // 8
}

I'm trying to understand how the compiler uses {1,2} in X x({1,2}). Followings are my understandings:

  • Uncomment line 1

    In such case, I think X x({1,2}) is an explicit call for X(initializer_list<int>). That is, when compiler sees {}-list, I think it will first see it as std::initializer_list and a constructor that takes an argument of initializer_list<T> will be the better match than all the others.

  • Comment out line 6, 7

    In such case, I think {1,2} is used to construct an object of Y which is bound with an rvalue reference (i.e. compiler chooses to call X(Y&&)). My guess here is that the compiler treats {1,2} as some kind of rvalue when there is no constructor that takes an argument of initializer_list<T>, and thus a constructor that takes an rvalue reference is preferred (in this case, X(Y&&) is the only constructor of this kind).

    Update: I found that the behavior I described in this bullet point is actually a bug of MSVC. gcc and clang will report ambiguity, not calling X(Y&&)

  • Comment out line 7

    Ambiguity arises. Compiler reports that both X(Y&&) and X(Y) match. I think this is because although X(Y&&) is a better match than X(const Y&) and X(const X&), it is indistinguishable to X(Y). (I think the logic here is quite twisted)

    Possible reference.

  • Comment out line 2,7

    The code compiles and prints param. I think this time X(const X&) is chosen but I'm not sure why. My guess for this is that, when all the viable matches (X(const X&), X(const Y&) and X(Y)) are indistinguishable, the compiler chooses X(const X&) because X x({1,2}); is constructing an X.

    Update: this can also be a MSVC bug. gcc and clang will report ambiguity, not calling X(const X&)

  • Comment out line 2,7 and uncomment line 8

    I think this is the case where all the viable matches are indistinguishable and f is not a constructor of X, hence f(const X&) is not treated specially, and ambiguity arises.

May I ask if my understandings are correct? (I highly doubt they are accurate)


I find that it is quite tedious to read out what constructor is invoked by things like X x({1,2});, so I guess in real code we should try to avoid such shorthand and write code in a more explicit way...


On the other hand, if we use the same settings for class X and Y as above, define and call a function g as follows:

X g()
{
    return { 1,2 };
}

int main()
{
    g();
}

The results always seem to be equivalent as initializing the returned temporary by X{1,2} (i.e. call X(initializer_list<int>) when it exists, and in all the other cases call X(int, int)). May I ask if this is true?

CPPL
  • 726
  • 1
  • 10
  • You should make a lot of the constructors explicit so they do not get used with implicit conversion. Writing `X x{1, 2};` should also cut down on the confusion. Generally the fewer constructors you have the better. – Goswin von Brederlow Jun 04 '22 at 05:48
  • @GoswinvonBrederlow Tell that to [`std::string`](https://en.cppreference.com/w/cpp/string/basic_string/basic_string)! – Paul Sanders Jun 04 '22 at 07:20

1 Answers1

1
  1. X x({1,2}) can’t be an “explicit call for” a std::initializer_list: a braced-init-list has no type. However, there is a rule saying that one is a better match for a specialization of that template than for any other class type.
  2. X(Y&&) is used because X(const X&) isn’t viable. It would be considered a second user-defined conversion even though {…}YX isn’t (because of the two layers of initialization).
  3. Yes, Q and Q&& are often ambiguous like that. Consider that C++17 prvalues don’t even move to initialize either.
  4. Presumably you meant you commented out lines 2 and 6. In that case, the constructors from (references to) X are disqualified as above, and X(const Y&) is a worse match because the const must be added to the non-const list-initialized prvalue. (All user-defined conversions are otherwise indistinguishable.)
  5. f(Y) is better than the other two regardless of X’s constructors for the same const reason.

As for g, yes copy-list-initialization is much the same as direct-list-initialization in the absence of explicit constructors. Since it is list-initialization, rather than direct initialization from an “argument” that is a braced-init-list, it does prefer an initializer-list constructor over all others.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76