Uniform initialization isn’t

Barry Revzin
5 min readSep 11, 2017

--

C++11 introduced list-initialization to us, a general term that refers to any initialization of an object with {}s. List-initialization offers many benefits, including:

  • giving us a way to avoid the most vexing parse
  • allowing for a way to initialize containers with a list of the objects they are intended to contain
  • allowing for a safer way of initialization that checks for narrowing

Another benefit often cited is that list-initialization offers us the same syntax for initializing an aggregate as it does for initializing a class type. This benefit has led to the adoption of the term uniform initialization to emphasize the idea that you only need to learn one syntax for initialization: the one with {}s. I’m here to argue against the idea of uniform initialization. I realize this has been done before, and more than once, but I’m also going to through in some C++17 examples too.

In most cases, initialization either requires {} (e.g. you cannot initialize an aggregate with ()s) or both {} and () do the same thing (e.g. most class types that aren’t aggregates). That, in itself, seems like a good argument in favor of uniform initialization. The problem case seems like a pretty small subset of the world: those cases in which list-initialization and non-list-initialization (a) both compile and (b) do different things. The canonical example of this phenomenon, and the one that likely appears in every blog and book that introduces list-initialization, comes to us from std::vector:

The reason this happens is that list-initialization for non-aggregate class types happens in two phases: first, we only consider those constructors that take a std::initializer_list and then, only if no viable constructor is found, do we consider the rest. For v, we find a constructor taking a std::initializer_list<int>, which works, so we’re done. It’s not that this is a better match than the constructor taking a size_t and an int const&, it’s that the latter is not even considered as an option. For w, the std::initializer_list constructor isn’t viable, since we’re not list-initializing, so the latter constructor is selected. This strong preference for initializer-list constructors can lead to quite a bit of confusion.

The potentially different behavior for something like initializing a vector<int> might seem like one of those things you just have to memorize to ace your next C++ interview, but it has pretty serious implications for how to write generic code. Consider trying to write a [completely pointless, yet illustrative] function template that just explicitly copies its argument:

Being paranoid of list-initialization in generic code is a good trait to have, especially when the template types can be arbitrary user types. You can’t be sure that’s going to happen.

This difference also now appears in more possible places, thanks to class template argument deduction. Consider the following:

What is the type of w2? The intent is probably for it to be a std::vector<int> but it’s actually a std::vector<std::vector<int>::iterator>! Of course. The reasoning is basically the same as in the first example: the initializer-list constructor is strongly preferred, and is now viable since we’re doing deduction. The initialization of w worked fine despite using the {}s, but one subtle change and that went away. Using ()s to initialize w2 would give us the desired deduction of std::vector<int>.

This example, too, may seem academic. But a very similar example was my first exposure to the most vexing parse, and depending on how you disambiguate with {}s, you could run into trouble again:

If I were to provide a guideline at this point, it would be:

Use parentheses to initialize in all cases. Use braces only for the specific behavior that braces provide.

This would limit your code using braces to those situations like aggregate-initialization and initializing containers with elements. If you always use {}s for only those situations, it will make those stand out more, which lets you emphasize these properly. Less uniform initialization, more conscious decisions about the code we right. One might ask at this point: why is the guideline to only use {} in certain situations rather than the other way around? Aren’t the two symmetrical? And the answer is that they’re not. Choosing {} would be a positive choice: you know when you’re initializing your type whether or not you need {}s. But choosing () would be a negative choice: you only need () when there exists a list-initializer constructor that you don’t want — which means you have to consider constructors that you don’t want to use but possibly exist when deciding how to use the construction you do want. This is more difficult to reason about.

Of course, one cannot be mindful about everything all the time, and C++ gives us so much that we already need to be mindful about. So a better guideline might be:

Avoid writing class interfaces in which such an ambiguity might arise.

The problem, ultimately, is that there are situations where ()s and {}s do different things. This is obviously a problem, but worse than that, it’s not that we even get any benefit from this. Consider a hypothetical different interface to std::vector that named its constructors:

Here, a and b both have 10 elements, and c and d both have 2 elements. It doesn’t matter what your initialization preference is, and the code even annotates itself.

I wanted to conclude here with an example from newest addition to the C++17 world, from a very recent correction to the language rules surrounding class template argument deduction. Consider the following example:

What should the types of t and v be? If you follow the guideline that {} for containers means something special — and that special meaning is producing a container containing the elements of that initializer list — what we get is:

  • t is a tuple<int, int> (since tuple is not a container, it does not have an initializer_list constructor — which makes list-initialization unnecessary and thus questionable),
  • the prvalue that v is initialized from, vector{1,2}, is a vector<int> with two elements (since vector is a container), and
  • v should be a vector<vector<int>> (since vector is a container).

That reasoning and that deduction was correct, at least until Toronto. There, P0702 was adopted, using this difference (that t has the same type as its initializer, but v doesn’t) as a motivating example. This paper changed the wording so that v is actually a vector<int>, as a separate exception to the deduction rules. This rule change has several odd repercussions. Consider:

Here, a is a vector<int> but b is a vector<vector<int>>, despite being initialized the same way. Meanwhile, c and d are both of type vector<int>, despite being initialized differently. As a result, vector is even more difficult to reason about when it comes to initialization. It’s just a stack (or, if you insist, a vector) of exceptions.

Ultimately, I am wary of the phrase uniform initialization. I think favoring {} leads to more confusion than not. I used to be in that camp, but I’ve gotten it wrong one too many times. I find it a lot easier to reserve the use of {} for those special cases where you need it. When you do so, it makes the braces stand out more, which is always a good thing. C++17’s addition of class template argument deduction makes the difference between list-initialization and non-list-initialization even more difficult to reason about, which is all the more reason to start thinking carefully and critically about how you use initialization in your code.

--

--