Why is the std::initializer_list constructor preferred when using a braced initializer list?
Asked Answered
B

3

39

Consider the code

#include <iostream>

class Foo
{
    int val_;
public:
    Foo(std::initializer_list<Foo> il)
    {
        std::cout << "initializer_list ctor" << std::endl;
    }
    /* explicit */ Foo(int val): val_(val)
    {
        std::cout << "ctor" << std::endl;
    };
};

int main(int argc, char const *argv[])
{
    // why is the initializer_list ctor invoked?
    Foo foo {10}; 
}

The output is

ctor
initializer_list ctor

As far as I understand, the value 10 is implicitly converted to a Foo (first ctor output), then the initializer constructor kicks in (second initializer_list ctor output). My question is why is this happening? Isn't the standard constructor Foo(int) a better match? I.e., I would have expected the output of this snippet to be just ctor.

PS: If I mark the constructor Foo(int) as explicit, then Foo(int) is the only constructor invoked, as the integer 10 cannot now be implicitly converted to a Foo.

Boling answered 26/11, 2014 at 8:9 Comment(4)
I knew it trumps over regular constructors, but didn't know that it trumps even when the regular constructor is a better match. And yes, it seems a bit strange to be this way. Is there any particular reason? In this way, one can hide the copy constructor (actually, the code I have WILL hide the copy constructor, doesn't it?)Boling
Scott Meyers' new book, "Effective Modern C++" has a very good item about the various initialization styles: "Item 7: Distinguish between () and {} when creating objects". It doesn't give much in the way of the rationale for the behavior, but does go into a lot of detail on some of the edge cases that might surprise you.Forsooth
@MichaelBurr thanks, I'm still waiting for a physical copy :)Boling
I know it's unrelated, but can anyone tell me whether I should have initializer_list by value or by const reference in my constructor ? And what is reason for that ?Norrie
O
31

§13.3.1.7 [over.match.list]/p1:

When objects of non-aggregate class type T are list-initialized (8.5.4), overload resolution selects the constructor in two phases:

  • Initially, the candidate functions are the initializer-list constructors (8.5.4) of the class T and the argument list consists of the initializer list as a single argument.
  • If no viable initializer-list constructor is found, overload resolution is performed again, where the candidate functions are all the constructors of the class T and the argument list consists of the elements of the initializer list.

If the initializer list has no elements and T has a default constructor, the first phase is omitted. In copy-list-initialization, if an explicit constructor is chosen, the initialization is ill-formed.

As long as there is a viable initializer-list constructor, it will trump all non-initializer-list constructors when list-initialization is used and the initializer list has at least one element.

Overspread answered 26/11, 2014 at 8:25 Comment(4)
Ok, +1, I think this nails it! For some reason I thought that init_list ctors won't trump if conversions are needed, but it doesn't seem to be the case. But, if this is the case, then the code I have seem to be hiding a potential copy constructor, doesn't it? As copy ctor will just copy the parameter for the init list ctor, and the latter can do whatever I desire.Boling
@Boling Well, this rule applies only if you are using list-initialization. When you don't use {} the copy ctor will be called just fine. An implicit special member function can indeed be "hidden". A common case is a template constructor taking a single forwarding reference argument optionally followed by a pack (e.g., template<class T> Foo(T&&)), which is usually a better match than a copy ctor taking a const Foo &.Overspread
@vsoftco, the code you have is only hiding a potential copy because you have Foo(std::initializer_list<Foo>) which makes no sense to me. What is that constructor for? Anyway, if you use a non-empty braced-init-list and there is an initializer-list constructor, it will be used, even if that requires an array of Foo objects to be created to bind the std::initializer_list to.Loudish
@JonathanWakely this was just a toy example, as I was trying to understand how std::initializer_list constructor works. I'm not using it in reality :)Boling
T
15

The n2100 proposal for initializer lists goes into great detail about the decision to make sequence constructors (what they call constructors that take std::initializer_lists) to have priority over regular constructors. See Appendix B for a detailed discussion. It's succinctly summarized in the conclusion:

11.4 Conclusion

So, how do we decide between the remaining two alternatives (“ambiguity” and “sequence constructors take priority over ordinary constructors)? Our proposal gives sequence constructors priority because

  • Looking for ambiguities among all the constructors leads to too many “false positives”; that is, clashes between apparently unrelated constructors. See examples below.
  • Disambiguation is itself error-prone (as well as verbose). See examples in §11.3.
  • Using exactly the same syntax for every number of elements of a homogeneous list is important – disambiguation should be done for ordinary constructors (that do not have a regular pattern of arguments). See examples in §11.3. The simplest example of a false positive is the default constructor:

The simplest example of a false positive is the default constructor:

vector<int> v; 
vector<int> v { }; // potentially ambiguous
void f(vector<int>&); 
// ...
f({ }); // potentially ambiguous

It is possible to think of classes where initialization with no members is semantically distinct from default initialization, but we wouldn’t complicate the language to provide better support for those cases than for the more common case where they are semantically the same.

Giving priority to sequence constructors breaks argument checking into more comprehensible chunks and gives better locality.

void f(const vector<double>&);
// ...
struct X { X(int); /* ... */ };
void f(X);
// ...
f(1);     // call f(X); vector’s constructor is explicit
f({1});   // potentially ambiguous: X or vector?
f({1,2}); // potentially ambiguous: 1 or 2 elements of vector

Here, giving priority to sequence constructors eliminates the interference from X. Picking X for f(1) is a variant of the problem with explicit shown in §3.3.

Toddler answered 26/11, 2014 at 8:34 Comment(0)
C
5

The whole initializer list thing was meant to enable list initialisation like so:

std::vector<int> v { 0, 1, 2 };

Consider the case

std::vector<int> v { 123 };

That this initializes the vector with one element of value 123 rather than 123 elements of value zero is intended.

To access the other constructor, use the old syntax

Foo foo(10);
Crosslink answered 26/11, 2014 at 8:12 Comment(2)
This makes sense, as both initialzier_list and regular ctors take int as parameters. I was surprised that even when a conversion is needed in the initializer_list ctor, the latter is still preferred.Boling
@Crosslink is the "old syntax" the only way to excape from the initializer_list. I mean, is it the standard way?Dearr

© 2022 - 2024 — McMap. All rights reserved.