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

  1. do not use uniform initialization for classes that may add a initializer_list constructor in the future,
  2. do not introduce an initializer_list constructor to a class that is currently being initialized using uniform initialization,
  3. 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:

  1. Make uniform initialization not prefer initializer_list constructors for template code.
  2. 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.