Let’s start with the popular C++(11) “uniform initialization” gotcha. Changing braces to parentheses in object initialization may change the semantics of the initialization:

std::vector<int> v1{5, 6}; // 2 elems: {5, 6} std::vector<int> v2(5, 6); // 5 elems: {6, 6, 6, 6, 6}

For instance, it is described in Problem 1 of the newly revisited “Guru of The Week” series by Herb Sutter.

When seeing such an example, one might conclude that the new rules for object initialization are very confusing or error-prone. But to me it looks like the problem here lies in how class template std::vector has been defined in C++03.

Given that std::vector<int> is meant to store int s, how come the constructor that takes two int s does not put them into the collection?! How come the following does not put integer value 100 into a container?

std::vector<int> v(100);

Even if you are familiar with this gotcha, and you are very careful which type of initialization (braces or parentheses) to use, the fact that these two initializations do different things may still impact you. Imagine that we are writing a wrapper around std::vector<int> . We would like to write a “perfect forwarding” constructor template, that generates for IntVecWrap all the constructors that std::vector<int> has, and forwards the arguments to the contained vector:

struct IntVecWrap { std::vector<int> data; template <typename... P> explicit IntVecWrap(P&&... v); // ... };

If you are not familiar with this syntax, this is a short explanation. The two ellipses say that this template is capable of rendering constructors taking any number or parameters (including 0). P is not a template type parameter, but a template parameter pack, representing a number of type parameters. Similarly, v is not a function parameter, but a function parameter pack, representing all arguments passed to our constructor. The && sign indicates that each single parameter in pack v will be perfect-forwarded, that is: instantiated as lvalue reference if the argument passed to the constructor is an lvalue, and as an rvalue reference if the passed argument is an rvalue, and an lvalue reference to const object, if the argument is a constant lvalue.

Now, how do we define the body of our constructor template? We have two options:

// option 1: template <typename... P> IntVecWrap::IntVecWrap(P&&... v) : data(std::forward<P>(v)...) {} // ^ // parentheses // option 2: template <typename... P> IntVecWrap::IntVecWrap(P&&... v) : data{std::forward<P>(v)...} {} // ^ // braces

Before discussing the options, a word of explanation of the syntax again. Function std::forward is necessary in perfect forwarding mechanism: it turns an rvalue reference into an rvalue (because rvalue reference function argument is an lvalue!). The third ellipsis indicates that we want to expand function parameter pack v into normal parameters using pattern std::forwrd<Pn>(an) , where Pn is the n -th template parameter and an is the n -th function parameter. Thus if our variadic template generated constructor:

IntVecWrap::IntVecWrap(size_t& a1, int&& a2);

the pack expansion in the constructor’s initialization list will be:

IntVecWrap::IntVecWrap(size_t& a1, int&& a2) : data( std::forward<size_t&>(a1), std::forward<int>(a2) )

Now, back to the selection of two options:

// option 1: template <typename... P> IntVecWrap::IntVecWrap(P&&... v) : data(std::forward<P>(v)...) {} // ^ // parentheses // option 2: template <typename... P> IntVecWrap::IntVecWrap(P&&... v) : data{std::forward<P>(v)...} {} // ^ // braces

Does it matter, which one we choose? It does, and neither does the right thing. If we choose option 1, we will get the following surprising effect:

// option 1: std::vector<int> v{6, 5}; IntVecWrap w{6, 5}; assert (v.size() == 2); assert (w.data.size() == 6); // !!!

This is because in the initialization list of IntVecWrap we change the braces to parentheses. Now, if we choose option 2, we will fix the above problem, but we will get another surprise:

// option 2: std::vector<int> v(6, 5); IntVecWrap w(6, 5); assert (v.size() == 6); assert (w.data.size() == 2); // !!!

This is because now we are changing parentheses into braces. We wouldn’t have this dilemma if vector ’s initialization were doing the same thing when brace- and parentheses-initialized. Then we could just pick any option and we would be fine.

We faced this problem when specifying the semantics of std::optional ’s forwarding constructors:

std::optional<std::vector<int>> o(std::in_place, 6, 5);

We couldn’t just say that this initializes the vector with arguments 6 and 5, because we do not know which initialization it should be: brace- or parentheses-initialization. We had to make the call which one it is. (We chose the parentheses one, you can see it in the newest Standard draft: N3690.)

Back to std::vector , and our one-argument constructor:

std::vector<int> v(100);

You might say: but we need some constructor that initializes the vector with the default capacity of 100. Default capacity, or was it default size? Damn, I always forget… This is because other libraries do provide a similar constructor that sets the initial capacity rather than size. For instance, java.util.ArrayList<E> . If someone switches from Java to C++, they will surely expect the following:

std::vector<int> v(100); assert (v.empty()); // invalid expectation

They will not even look into the documentation, because they will fill they know what it does.

I guess the reasoning behind adding this constructor was: “in order to construct a vector with the given size I only need to pass it one value of type size_t . So let such one-argument constructor create an n -sized vector with value-initialized elements.” But when you look at it from the other side, i.e. user’s side, when you see such one-argument constructor taking parameter of type size_t , you are puzzled, because it can do a couple of things: create a 0-sized vector with capacity of n , create n -sized vector, create 1-sized vector with value n , or whatever else. Yes, you can look it up in the docs, but it is not clear immediately: it is just not intuitive.

One could argue that vector<int> is a very special case: I wouldn’t notice the braces-vs-parentheses problem problem if it was vector<string> . True, but I would still be confused whether the number indicates the initial size or initial capacity or initial “something else.” This latter problem is more general.

How to fix it?

This problem would be easily fixed if we had a feature known as named parameters. Then we could initialize the vector like this:

// NOT IN C++ std::vector<int> v1(size: 10, capacity: 200, value: 6);

But well, we don’t have them (although there is Boost.Parameter library); and it is not clear how would perfect forwarding look for functions/constructors with named parameters. However, there is a decent substitute for this feature: function ‘tag’ parameters. Tags are empty classes that do not contain any numeric data, but represent a different type that can be used to disambiguate different function/constructor overloads that would otherwise be ambiguous. If we had tags representing size and capacity of STL containers, like:

namespace std{ constexpr struct with_size_t{} with_size{}; constexpr struct with_value_t{} with_value{}; constexpr struct with_capacity_t{} with_capacity{}; }

We could initialize the containers in a way that is completely unambiguous (although a bit verbose):

std::vector<int> v1(std::with_size, 10, std::with_value, 6); std::vector<int> v2{std::with_size, 10, std::with_value, 6};

Such code takes longer to write, but takes less time to read and understand, and avoids potential confusion.

Tags are nothing new. They are already in C++ Standard Library. Consider:

std::function<void()> f1{a}; std::function<void()> f2{std::allocator_arg, a};

std::allocator_arg is a tag indicating that the following argument is to be treated as an allocator, rather than a callable object. Next:

std::tuple<std::string, int, float> t1 = /*...*/; std::tuple<float, float, std::string> t2 = /*...*/; std::pair<X, Y> p1{t1, t2}; std::pair<X, Y> p2{std::piecewise_construct, t1, t2};

Here, p1 is initialized as:

p1.first(t1), p1.second(t2);

p2 is initialized as:

p2.first(get<0>(t1), get<1>(t1), get<2>(t1)), p2.second(get<0>(t2), get<1>(t2), get<2>(t2));

Next:

std::optional<std::string> o1{}; std::optional<std::string> o2{std::in_place};

The former creates an object that stores no string; the later creates an object that stores a string which is empty. Next:

std::mutex m; std::unique_lock<std::mutex> l1{m}; std::unique_lock<std::mutex> l2{m, std::defer_lock};

The former does lock the mutex, while the latter does not.

Of course, tags are only needed when the meaning of the initialization would otherwise be unclear. For instance the following would be silly:

// not in C++ complex<double> z1{real_part, 1.0}; complex<double> z2{real_part, 1.0, imaginary_part, 0.0};

Tags are interesting technique to consider for constructors. With other functions we can usually use different names for different behaviour. For initialization, we do not have this option: all constructors bare the same name, so we can use the tags to somehow change constructor name. However, tags are not an ideal solution, because they pollute the namespace scope, while being only useful inside function (constructor) call.