The problems with uniform initialization
by Malte Skarupke
C++11 made the {} syntax for initializing aggregates more widely usable. You can now also use it to call constructors and to initialize with a std::initializer_list. It also allows you to drop the type name in some circumstances. The general recommendation seems to be that you use it as much as possible. But when I have started doing that I have found that it sometimes doesn’t do what I want, and that it may make maintenance more difficult.
Here’s what it looks like:
#include <iostream> struct Widget { Widget(int m) : m{m} {} operator int() const { return m; } int m; }; Widget decrement_widget(Widget w) { return { w - 1 }; } int main() { int a{1}; std::cout << a << std::endl; // "1" Widget w{5}; std::cout << w << std::endl; // "5" std::cout << decrement_widget({5}) << std::endl; // "4" }
It can be used to initialize everything (hence the name uniform initialization) and as you can see it makes some code more convenient because I don’t even need the name any more if the compiler should know it.
It makes your code look a bit weird at first because you have squiggly braces everywhere, but after a while I found that I prefer it because it sets initialization apart from function calls.
But I have stopped using it and I recommend that you don’t use it either.
The reason comes from the fact that it prefers std::initializer_list constructors over other constructors. This is what using an initializer_list constructor looks like:
#include <iostream> #include <vector> int main() { std::vector<int> v{ 10, 20, 30 }; // prints 10 20 30 for (int i : v) { std::cout << i << ' '; } std::cout << std::endl; }
Pretty convenient, right? This is actually the one thing that I recommend you use uniform initialization for. In other cases I recommend that you prefer () initialization.
The problem is most obvious for template code. Consider for example this:
template<typename T> std::vector<T> create_ten_elements() { return std::vector<T>{10}; } int main() { create_ten_elements<std::string>(); // create ten elements create_ten_elements<int>(); // create one element create_ten_elements<Widget>(); // same Widget as above. creates one element create_ten_elements<char>(); // create one element create_ten_elements<std::vector<int>>(); // create ten elements }
If I had used () initialization, that line would have always created a vector with ten elements. This means that for example std::allocator<T>::construct can not use uniform initialization, because you wouldn’t be able to call certain constructors. And that is a total shame because that is precisely the one place where uniform initialization would be really useful.
The problem is that when you’re dealing with templates, you have no idea what uniform initialization will do. Not even something simple like this always does the same thing:
template<typename T> T copy(const T & to_copy) { return T{ to_copy }; } int main() { copy(std::string{}); // returns a copy copy(std::vector<std::function<void ()>>{}); // compiler error copy(std::vector<boost::any>{}); // does not return a copy }
The second example in there will be fixed when everyone implements the solution to LWG 2132. The third line will never be fixed because anything is convertible to a boost::any.
But it would make sense to use uniform initialization in std::allocator<T>::construct. It would for example allow you to put aggregates into std::vectors. The most likely solution for that is that std::enable_if will be used as described in LWG 2089.
So for template code I recommend that you do not use uniform initialization. If you want to use it, only do it behind a std::enable_if that forces () initialization when available and only uses {} as a fallback.
But the problem exists also for non-template code. Imagine that you’ve been using uniform initialization with the above Widget class. And then someone goes and introduces the following std::initializer_list constructor:
struct S { S(int) {} }; Widget(std::initializer_list<S>) {}
This has just broken all of the constructor calls in the first example plus a few more. Not even the copy constructor is available any more through uniform initialization because the compiler will prefer to call operator int() and then call the initializer list constructor. And the worst thing is that it doesn’t even give you a warning.
Because of that I recommend that you
- do not use uniform initialization for classes that may add a initializer_list constructor in the future,
- do not introduce an initializer_list constructor to a class that is currently being initialized using uniform initialization,
- do not have conversion operators on classes that are passed as arguments to uniform initialization, (except if it’s an explicit conversion operator)
The third one is also a problem for () initialization, but not as much. Because in () initialization you can only change the behavior of existing code by introducing a constructor that is a better match. Because of that an additional conversion operator will at worst give you a compiler error instead of a silent change in behavior. But {} initialization will happily switch to use the conversion operator if it means that it can use an initializer_list constructor, even if a direct match is available.
So that’s already quite restricting. But just in general I find that uniform initialization has a tendency to not do what I want it to do when I least expect it. For example
#include <string> int main() { // create the string "aaa" std::string aaa(3, 'a'); // create the string "\x3a" std::string notaaa{ 3, 'a' }; }
So my recommendation is this: Do not use uniform initialization except if you want initializer_list initialization.
This is obviously not what was intended. The big advantage is that you can use uniform initialization for everything. Unfortunately I think doing that will create problems for you in the long term. Especially the problem with template code will be a mess, because that is precisely the thing that people will want to use uniform initialization for. And then it will break one day on a type that you didn’t expect it to break on. And you can’t change it easily without breaking other code.
So can this be fixed?
After I pointed out these problems on the std-proposals mailing list someone started a very long thread in which several new syntaxes were proposed. I don’t like those ideas because they would essentially deprecate the current syntax in favor of a new one with the proper rules. Also I think that adding more ways to initialize objects is a terrible idea. Instead the current syntax should just be fixed, even if that means breaking code.
All that is needed is to say that uniform initialization does not prefer initializer_list constructors. If they are considered by the normal rules of overload resolution then this problem goes away. Unfortunately that has the potential to silently break a lot of fancy new C++11 code, so it’s a bit scary.
So I think two changes are needed:
- Make uniform initialization not prefer initializer_list constructors for template code.
- For non-template code and for fully specialized templates, only prefer initializer_list constructors if they do not require a conversion.
If the initializer_list preference is only changed for template code, then that will break a tiny amount of existing code. The code that this will break probably didn’t do what you wanted it to do anyway. The second change could break more code but in most cases it would give you a compiler error that is easy to fix, so I think that’s OK.
These changes would mean that std::vector<T>{10}; will always create a vector with ten elements, while std::vector<int>{10}; will create a vector with one element because it is fully specialized and no conversion is required. I think that this is the behavior that you would expect, and I think it will also give the behavior you’d expect for other cases. The disadvantage is that this will make the rules rather more complex, but hopefully you don’t have to read the rules if it does what you expect more often than not.
“These changes would mean that std::vector<T>{10}; will always create a vector with ten elements, while std::vector<int>{10}; will create a vector with one element because it is fully specialized and no conversion is required. I think that this is the behavior that you would expect, and I think it will also give the behavior you’d expect for other cases.”
I don’t know. I would expect template code to behave exactly like the equivalent non-templated code. I expect `std::vector<T> …` to do the exact same thing as `std::vector<int> …` if `T = int`, no matter what the `…` part is.
You are proposing that template code will no longer function the same way as its post-substitution equivalent non-template code. Nowhere in C++ is that the case (outside of template-specific stuff like SFINAE). Once the template arguments are substituted in place, the code works exactly like regular non-templated code, using all the usual rules thereof.
Your suggestion represents a pretty unprecedented change in C++.
Good point, and that would suck. But it’s better than not being able to use this syntax with templates at all. Which is pretty much the current situation, because you simply don’t know what your code will do. And even if your code does currently do what you expect it to do, you put new constraints on all classes used to instantiate your template code so that they don’t suddenly break in the future.
I think what will happen is that either people will adopt behavior to not use this syntax in template code, or everyone will have to use SFINAE. Both of which are not good.
A poor decision was made with the preference of initializer_list constructors and more poor decisions will have to be made to clean that up. I think mine is the least bad.
Great post! I was very confused about uniform initialization until reading your post.
It seems all the troubles are caused by dynamics of uniform initialization. An uniform initialization call initializer_list constructor if possible, otherwise fallback to other constructor. The problem is that initializer_list constructor and other constructer are often totally different, so different that they should be named differently, say create_n and create_from_values, if they were not constructers. On the other hand, the meaning of function call in template must be clear, without seeing the definition of template parameters.
Do you know why do the if…else behavior is adopted by uniform initialization? We can just use
vector{{10}} for initializer_list and vector{10} for the other constructors.
You can use vector({10}) and vector(10). I prefer it. I used to be lazy with it only when there are obviously too many elements to be confused with a normal constructor like v = {10, 20, 30, 40};. I did not have any detailed knowledge on the matter even after reading / watching lessons. I just felt, that this is something nasty and I have to avoid using it until the dust settles. Similar madness goes on with other “features” too.