2.1.5 Uniform initialization

What was the problem

There are so many ways to initialize a variable in C++ that it became a running gag, so it’s natural that a new initialization syntax was added into C++11.

Anyway, let’s jump to the problem. The following code is a recurring issue in C++, so much that even experienced programmers stumble upon it once in a while.

struct foo {}; 
 
struct bar 
{ 
  bar(foo f); 
 
  int i; 
}; 
 
int main() 
{ 
  bar foobar(foo()); 
  printf("%d\n", foobar.i); 
 
  return 0; 
}

The question is “what is foobar in the above code?” Most C++ programmers will say it is a bar constructed with a default-constructed foo. Unfortunately it is actually the declaration of a function returning bar and taking a function returning a foo as its single argument. This problem is known as the most vexing parse.

A workaround for this issue is to declare a temporary of type foo and pass it to the constructor of bar:

int main() 
{ 
  foo f; 
  bar foobar(f); 
  printf("%d\n", foobar.i); 
}

This is not very convenient, and suddenly the scope is polluted by a useless variable. Moreover it can quickly become hard to implement when a variable number of arguments are passed to foobar (e.g. by forwarding the arguments of a variadic macro, or a variadic template 2.3.3).

How the Problem is Solved

Enters the uniform initialization from C++11. By replacing the parentheses with brackets in the construction of the argument, the ambiguity is lifted.

int main() 
{ 
  bar foobar(foo{}); 
  printf("%d\n", foobar.i); 
}

Used like that, the brackets mean something like “a default-created foo”. It can also be used to zero-initialize any variable, which is especially nice in a template context:

template<typename T> 
void many_tees() 
{ 
  // If T is a class, calls the default constructor. 
  // If T is a fundamental type (e.g. int), its value is 
  // whatever is in memory at &t1. 
  T t1; 
 
  // Declares a function t2 with no argument and returning 
  // a T. 
  T t2(); 
 
  // Seems to work. Does it? 
  T t3 = T(); 
 
  // If T is a class, calls the default constructor. 
  // If T is a fundamental type (e.g. int), its value is 
  // the zero of this type. 
  T t4{}; 
}
When to Use the Uniform Initialization Syntax

Consider the code below:

#include <cstdio> 
#include <utility> 
 
struct foo_struct 
{ 
  int a; 
  int b; 
}; 
 
struct foo_constructor 
{ 
  foo_constructor(int v1, int v2) 
    // Note that arguments and fields are swapped. 
    : a(v2), b(v1) 
  {} 
 
  int a; 
  int b; 
}; 
 
struct foo_initializer_list 
{ 
  foo_initializer_list(int v1, int v2) 
    // Note that arguments and fields are still swapped. 
    : a(v2), b(v1) 
  {} 
 
  // An initializer list allows to construct an object using an aggregate-like 
  // syntax. We will come back to it in 2.5.6 :) 
  foo_initializer_list(const std::initializer_list<int>& i) 
    // Note that the fields are set to the same value. 
    : a(*i.begin()), 
      b(a) 
  {} 
 
  int a; 
  int b; 
}; 
 
template<typename Foo> 
void build_foo(int x, int y) 
{
  Foo foo{x, y};
 
  printf(".a=%d, .b=%d\n", foo.a, foo.b); 
} 
 
int main() 
{ 
  build_foo<foo_struct>(24, 42); 
  build_foo<foo_constructor>(24, 42); 
  build_foo<foo_initializer_list>(24, 42); 
 
  return 0; 
}

What would be the output of this program? The highlighted line constructs a variant of foo with what looks like the aggregate initialization syntax, or is it uniform initialization? Maybe is it a call to a constructor? Which one?

Here is the output of this program:

$ a.out 
.a=24, .b=42 
.a=42, .b=24 
.a=24, .b=24

So the first call is without surprise an aggregate initialization.

The second one is a call to the constructor; since there is one defined for foo_constructor then the aggregate initialization is disabled. Note that before C++11 the compiler would have reported an error, saying that the constructor should be used. Here it calls the constructor silently even though it looks like an aggregate initialization.

The last one is the worst. It creates an std::initializer_list 1 with the values, then pass it to the corresponding constructor.

This is painfully ambiguous. Unfortunately, the so-called “modern” C++ programming trend is pushing for using bracket initialization, maintaining ambiguities everywhere. I prefer to use it parsimoniously. In particular, uses of std::initializer_list should certainly be avoided as they lure the programmer into thinking that the assignment is an efficient aggregate initialization while it is actually copying stuff around.

Guideline

Use bracket initialization if:

Do not use bracket initialization if you want to call a non-default constructor.

As a side note, the bracket initialization can be used without specifying the type, for example to assign a variable or for the return statement of a function:

struct interval 
{ 
  interval(); 
  interval(int a, int b); 
}; 
 
interval build_interval(bool f) 
{ 
  interval b; 
 
  if (f) 
    b = {23, 23}; 
 
  return {42, 32}; 
}

This is terrible.

Guideline

Mind the next reader, write what you mean.

While the compiler can effortlessly find the type of a variable, or the type returned by a function, a human will either have to scan back toward the declarations or be helped by a tool, if they have one.

Constructing a value without saying the type is ambiguous for a human. This is a lot of effort to put on the reader to save some characters on the writer’s side.

Don’t build complex types without explicitly tell what you want to build.

1See section 2.5.6 for details about that.