20

Let's consider the following:

#include <iostream>
#include <initializer_list>

class Foo {
public:
    Foo(int) {
        std::cout << "with int\n";
    }
};

int main() {
    Foo a{10};  // new style initialization
    Foo b(20);  // old style initialization
}

Upon running it prints:

with int
with int

All good. Now due to new requirements I have added a constructor which takes an initializer list.

Foo(std::initializer_list<int>) {
    std::cout << "with initializer list\n";
}

Now it prints:

with initializer list
with int

So my old code Foo a{10} got silently broken. a was supposed to be initialized with an int.

I understand that the language syntax is considering {10} as a list with one item. But how can I prevent such silent breaking of old code?

  1. Is there any compiler option which will give us warning on such cases? Since this will be compiler specific I'm mostly interested with gcc. I have already tried -Wall -Wextra.
  2. If there is no such option then do we always need to use old style construction, i.e. with () Foo b(20), for other constructors and use {} only when we really meant an initializer list?
taskinoor
  • 45,586
  • 12
  • 116
  • 142
  • Perhaps the correct way of handling this would be to decide at the onset whether you want a `initializer_list` constructor or not. It should be relatively obvious, since they are typically containers, or am I missing something – Passer By Sep 25 '17 at 05:57
  • 10
    Why was this down-voted? It's well phrased and is likely to be of use to future readers. Seriously people, down-voting is meant to reflect post quality, not how much you like or dislike the topic. – StoryTeller - Unslander Monica Sep 25 '17 at 05:59
  • @PasserBy let's say something like vector. You can take a size and also a list of elements. Before adding list constructor you had only size. After adding list constructor that size suddenly become element and I'm in trouble. – taskinoor Sep 25 '17 at 06:01
  • @taskinoor Which is why I said at the __onset__, not sometime after. – Passer By Sep 25 '17 at 06:02
  • 1
    The answer is (2), in the examples you've shown there's no reason to use braces instead of parentheses. And as `vector` has demonstrated, adding an `initializer_list` constructor to an existing interface is not something to be done unless the benefits are significant. – Praetorian Sep 25 '17 at 06:02
  • @PasserBy can you please elaborate what you meant by `onset`? When we are writing the class the first time? But that may not be the case always, specially if you inherit some old code. – taskinoor Sep 25 '17 at 06:04
  • This is a interface design, and as you have demonstrated, adding a `initializer_list` overload breaks the interface. As all interface design goes, you decide on a contract __at the beginning__, and don't break it unless it has spectacular reasons. If you do however, you will need to broadcast that loud and clear. – Passer By Sep 25 '17 at 06:04
  • 2
    Old code doesn't suffer from this problem, list initializing is a feature of C++11, which is also why the standard library never had problems – Passer By Sep 25 '17 at 06:05
  • @PasserBy yes I agree that it would have been better if this was designed properly from the beginning. But this question is exactly for the opposite case, i.e. when you don't have that good design from the beginning. – taskinoor Sep 25 '17 at 06:05
  • By old I didn't mean codes before C++11. Not that much old :-) – taskinoor Sep 25 '17 at 06:06
  • Then you just don't break it. End of story. Unless you have very good tools to replace all occurrences of such bugs. – Passer By Sep 25 '17 at 06:06
  • 2
    @PasserBy - Hindsight is 20/20, as they say. It's quite patronizing to say "End of stroy". Code bases along with requirements change and evolve. You can't always future proof yourself. – StoryTeller - Unslander Monica Sep 25 '17 at 06:10
  • If you are considering change in the class API do not add `Foo(std::initializer_list)` since it breaks old code - add `Foo(int, std::initializer_list)` and allow to pass 0 for int indicating that size should be taken from initializer list size – Artemy Vysotsky Sep 25 '17 at 06:13
  • 2
    I apologize if it sounds patronizing, its just what I'm thinking to myself at the moment. I should've chosen words more carefully – Passer By Sep 25 '17 at 06:14
  • Can't you use the `int` constructor behavior for `std::initializer_list`s if `size() == 1` ? – Hatted Rooster Sep 25 '17 at 14:15
  • @RickAstley: a constructor can call another constructor only in the member initialization list, and you can't make a decision about whether or not to call a constructor conditionally from there. What you can do, though, is move the `int` constructor code to a member method, and then have both constructors decide whether to call that method. – Remy Lebeau Sep 25 '17 at 15:25
  • @RemyLebeau exactly what I meant. – Hatted Rooster Sep 25 '17 at 16:39

3 Answers3

5

It's impossible to generate any warning in these cases, because presented behaviour of choosing std::initializer_list constructor over direct matches is well defined and compliant with a standard.

This issue is described in detail in Scott Meyers Effective Modern C++ book Item 7:

If, however, one or more constructors declare a parameter of type std::initializer_list, calls using the braced initialization syntax strongly prefer the overloads taking std::initializer_lists. Strongly. If there’s any way for compilers to construe a call using a braced initializer to be to a constructor taking a std::initializer_list, compilers will employ that interpretation.

He also presents a few of edge cases of this issue, I strongly recommend reading it.

Outshined
  • 709
  • 7
  • 22
3

I couldn't find such an option, so apparently in such cases you should use parentheses for classes that have initializer_list constructor, and uniform initialization for all other classes as you wish.

Some useful insights can be found in this answer and comments to it: https://stackoverflow.com/a/18224556/2968646

lomereiter
  • 356
  • 1
  • 5
  • Looks like the answer you linked describes the exact situation that I'm asking about. And it seems that there isn't much other option to prevent it. – taskinoor Sep 25 '17 at 11:51
2

There are no compiler warnings and there never will be. It just doesn't make sense to warn on code doing something common like

std::vector vec{1};

Remember that the compiler only warns about really unwanted stuff, like undefined behavior. It has no way of knowing that in the definition above, you meant to call the constructor taking a size argument. For all it knows you actually want to have a vector with one element! It can't read your mind :)

The answer to your second question is basically yes. You can always add a dummy parameter like struct {} dummy; to avoid using the constructor with initializer list, but really, the only same solution is just to use parentheses instead of braces (or don't break the interface suddenly).

If you want to change every portion of code that uses list initialization, you can delete the initializer list constructor, change them to braces, and then implement the constructor correctly. I would consider such change a breaking one, and deal with it appropriately. The other idea would have been to come up with the initializer list use case beforehand, and implement it right away.

Rakete1111
  • 47,013
  • 16
  • 123
  • 162