13

Suppose I have a class like this:

class Foo
{
public:
    Foo(int something) {}
};

And I create it using this syntax:

Foo f{10};

Then later I add a new constructor:

class Foo
{
public:
    Foo(int something) {}
    Foo(std::initializer_list<int>) {}
};

What happens to the construction of f? My understanding is that it will no longer call the first constructor but instead now call the init list constructor. If so, this seems bad. Why are so many people recommending using the {} syntax over () for object construction when adding an initializer_list constructor later may break things silently?

I can imagine a case where I'm constructing an rvalue using {} syntax (to avoid most vexing parse) but then later someone adds an std::initializer_list constructor to that object. Now the code breaks and I can no longer construct it using an rvalue because I'd have to switch back to () syntax and that would cause most vexing parse. How would one handle this situation?

void.pointer
  • 24,859
  • 31
  • 132
  • 243
  • 3
    Some people do not recommend it for this and other reasons. (Btw, you're right that the `intializer_list` ctor will be preferred.) – dyp Sep 18 '14 at 17:26
  • @dyp But then I wonder, what's the point? The most vexing parse and narrowing prevention are very good reasons to use brace-init syntax. But now I have to choose one annoyance or another? – void.pointer Sep 18 '14 at 17:26
  • 4
    Exactly. It has been an attempt at creating one syntax for all initializations, but that necessarily has drawbacks if there are multiple ways the initialization can be performed -- there has to be a disambiguation, and that probably cannot be perfect. – dyp Sep 18 '14 at 17:29
  • 1
    I'd say it'd be a bad design decision to later add an `initializer_list` constructor that has elements of the same type as other constructors. If you have to do that, you should probably add a tag parameter or something to the other constructors for disambiguation. – Praetorian Sep 18 '14 at 17:32
  • 1
    @Praetorian See `vector`. – dyp Sep 18 '14 at 17:32
  • The most vexing parse can typically be overcome by adding some more `()`. – dyp Sep 18 '14 at 17:33
  • 1
    @dyp Yeah, I know. I even typed "a prime example is `std::vector`" and then decided against it :) – Praetorian Sep 18 '14 at 17:34
  • @Praetorian Can you add an example to go with your explanation (about adding a tag)? That would be very helpful. Thank you. – void.pointer Sep 18 '14 at 17:37
  • I was thinking of something along the lines of the [`std::pair` constructor](http://en.cppreference.com/w/cpp/utility/pair/pair) that takes an argument of type `std::piecewise_construct_t`. Sorry, I don't have time to post an answer right now. And I'm not sure this is the best solution to this problem in all situations. – Praetorian Sep 18 '14 at 17:42
  • That's ok, the examples that are the basis of my question are contrived. I am not sure if I would actually run into this in practice. And as you said, if I did, it's probably bad design and can be easily refactored. – void.pointer Sep 18 '14 at 17:46
  • 3
    see [this Q&A](http://stackoverflow.com/q/19847960/819272) for a list of surprising examples in the Standard Library. Just make sure you don't fall into them, but otherwise I would still prefer `{}` over `()` because `{}` can be used everywhere. – TemplateRex Sep 18 '14 at 19:56

2 Answers2

2

What happens to the construction of f? My understanding is that it will no longer call the first constructor but instead now call the init list constructor. If so, this seems bad. Why are so many people recommending using the {} syntax over () for object construction when adding an initializer_list constructor later may break things silently?

On one hand, it's unusual to have the initializer-list constructor and the other one both be viable. On the other hand, "universal initialization" got a bit too much hype around the C++11 standard release, and it shouldn't be used without question.

Braces work best for like aggregates and containers, so I prefer to use them when surrounding some things which will be owned/contained. On the other hand, parentheses are good for arguments which merely describe how something new will be generated.

I can imagine a case where I'm constructing an rvalue using {} syntax (to avoid most vexing parse) but then later someone adds an std::initializer_list constructor to that object. Now the code breaks and I can no longer construct it using an rvalue because I'd have to switch back to () syntax and that would cause most vexing parse. How would one handle this situation?

The MVP only happens with ambiguity between a declarator and an expression, and that only happens as long as all the constructors you're trying to call are default constructors. An empty list {} always calls the default constructor, not an initializer-list constructor with an empty list. (This means that it can be used at no risk. "Universal" value-initialization is a real thing.)

If there's any subexpression inside the braces/parens, the MVP problem is already solved.

Potatoswatter
  • 134,909
  • 25
  • 265
  • 421
0

Retrofitting classes with initializer lists in updated code is something that sounds like it will be a common thing to happen. So people start using {} syntax for existing constructors before the class is updated, and we want to automatically catch any old uses, especially those used in templates where they may be overlooked.

If I had a class like vector that took a size, then arguably using {} syntax is "wrong", but for the transition we want to catch that anyway. Constructing C c1 {val} means take some (one, in this case) values for the collection, and C c2 (arg) means use val as a descriptive piece of metadata for the class.

In order to support both uses, when the type of element happens to be compatible with the descriptive argument, code that used C c2 {arg} will change meaning. There seems to be no way around it in that case if we want to support both forms with different meanings.

So what would I do? If the compiler provides some way to issue a warning, I'd make the initializer list with one argument give a warning. That sounds tricky not to mention compiler specific, so I'd make a general template for that, if it's not already in Boost, and promote its use.

Other than containers, what other situations would have initializer list and single argument constructors with different meanings where the single argument isn't something of a very distinct type from what you'd be using with the list? For non-containers, it might suffice to notice that they won't be confused because the types are different or the list will always have multiple elements. But it's good to think about that and take additional steps if they could be confused in this manner.

For a non-container being enhanced with initializer_list features, it might be sufficient to specifically avoid designing a one-argument constructor that can be mistaken. So, the one-arg constructor would be removed in the updated class, or the initializer list would require other (possibly tag) arguments first. That is, don't do that, under penalty of pie-in-face at the code review.

Even for container-like classes, a class that's not a standard library class could impose that the one-arg constructor form is no longer available. E.g. C c3 (size); would have to be written as C c3 (size, C()); or designed to take an enumeration argument also, which is handy to specify initialized to one value vs. reserved size, so you can argue it's a feature and point out code that begins with a separate call to reserve. So again, don't do that if I can reasonably avoid it.

JDługosz
  • 5,592
  • 3
  • 24
  • 45