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?

2
  • 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. Commented Jun 4, 2022 at 5:48
  • @GoswinvonBrederlow Tell that to std::string! Commented Jun 4, 2022 at 7:20

1 Answer 1

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.