Initializing Objects in C++

The Problem

C++ offers a variety of syntaxes for creating objects, and they all usually do the same thing. That "usually" is where things get tricky. To tackle this problem, we will have to consider the problem in three parts. At this point, this article completely ignores yet another method of initialization, aggregate initialization.

Assumptions

This article assumes that you are programming in C++14 (C++1y).

The advice also assumes that when calling a constructor, you know at the call site whether you want to construct your object with an initializer list or not. Forwarding functions should follow the advice as though they do not want to call an initializer_list constructor because this gives the user the most control. They can explicitly construct a std::initializer_list rather than relying on a particular syntax at the constructor call site, but it is not possible to say "Don't treat this as an initializer_list" if the forwarding function makes the opposite choice.

Zero, One, Many

Zero arguments

Consider the ways to construct an object with no arguments. Here, we've already run into trouble. Which type of initialization do you want? Zero initialization, default initialization, or value initialization? Or perhaps you want to call an initializer list constructor with no elements in the list? If that isn't enough, the legal syntax is slightly different if constructing a temporary object.

Construction of a non-temporary object

// Default initialization, does nothing for built-in types
T a;

// Value initialization, initializes built-in types to 0 or calls the default
// constructor for class types. If there is no default constructor, this calls
// an initializer list constructor with no arguments.
T b{};

// Same as above
T c = {};

// Copy initialization. Requires a visible move constructor and never calls an
// initializer list constructor. Zero-initializes POD types and calls the
// default constructor of class types.
auto d = T();

// Copy initialization. same as above except it can call an initializer list
// constructor if there is no default constructor.
auto e = T{};

// Calls an initializer list constructor with a 0-element initializer list
T f({});

// Same as above
T g{{}};

If the goal is to initialize a non-movable type by calling its default constructor or initialize a built-in type to 0, there is no immediately obvious way to do so. Because the form used on b can call an initializer list constructor, it could do the wrong thing when default initialization is desired. However, the following trick should protect against that:

static_assert(
	std::is_default_constructible::value,
	"This function can only be called with default-constructible types."
);
T b{};

The form used by c has the same effect as the above as long as the move constructor is visible (the move constructor will not actually be called). It would seem that e adds no value, other than possibly improving uniformity with other code. To me, f is a bit easier to read than the double braces in g, but that is purely a matter of style.

Note that the syntax `T h();` is simply wrong. It does not construct a variable of type T, it declares a function named h that returns a T. Fortunately, clang warns about this with -Wvexing-parse, but gcc is silent even if you turn on a very high warning level.

Construction of a temporary object

Constructing a temporary object opens up some new options and removes some others.

void fun1(T &&);
void fun2(T);

// Option a. Only calls default constructor
T()

// Option b. Only calls the default constructor
(T())

// Option c. First tries default constructor, then initializer list
T{}

// Option d. Only calls initializer list
T({})

// Option e. Only calls initializer list
T{{}}

// Option f. First tries default constructor, then initializer list
{}

// Option g. Only calls initializer list.
{{}}

All of these options can safely be passed to fun1 even if the type is non-movable and non-copyable. Options e and f have the unique distinction of also working for fun2 for non-movable and non-copyable types.

Note that a and b only differ by the surrounding parentheses. Every option but f and g can actually be surrounded in extra parentheses, but only in option a does it ever make a difference. a and b are the only options to call the default constructor and nothing else, but a has a pitfall. Once again, we are bitten by the most vexing parse.

Consider a class that can accept a single argument of some type, and we want to default construct that type (and never call an initializer list constructor). The straightforward approach would look something like this:

U u(T());

This uses version a to construct a temporary T and pass it to the constructor of U. At least, that is what it is trying to do. gcc happily compiles this, but once again clang warns with -Wvexing-parse. Adding the 'extra' parentheses in option b solves the problem and forces this to be a variable declaration.

Those extra parentheses, however, are likely to confuse people, as it is not immediately obvious why they are needed. For symmetry with constructing a non-temporary, it is probably best to use option c with a static_assert.

One argument

There are many ways to initialize an object from a single argument. First we will again start with the temporary object constructors.

Construction of a temporary object

// Option a. Functional cast. Allows explicit constructor calls, but also
// performs many other usually undesirable operations such as const_cast. Only
// works for single word types.
T(arg)

// Option b. Same as above
(T(arg))

// Option c. Same as above, but works with multi-word type specifiers such as
// unsigned int or int *.
(T)arg

// Option d. static_cast is a safer alternative to the above casts. It does not
// cast away const or volatile qualifiers.
static_cast(arg)

// Option e. Will call an initializer_list constructor if it exists, otherwise
// calls a single argument construtor. Will not perform narrowing conversions.
T{arg}

// Option f. Will only call an initializer_list constructor with a single value.
T({arg})

// Option g. Same as above
T{{arg}}

// Option h. Implicit conversion. This will only construct a T if the target
// location (for instance, a function parameter) specifies the type. Requires
// that T has an implicit constructor or arg has an implicit conversion
// operator.
arg

// Option i. Same as option e, but with the same restrictions as option h.
{arg}

Due to fairly obvious safety issues, options a, b, and c should all be avoided. a also has its own most vexing parse problem that is resolved by option b, but it will not come up if you do not use it.

If the goal is to only call an initializer_list constructor, f and g are identical. They are your safest option because all of the others can call constructors that do not take an initializer list.

If you want to always call a single-argument constructor that is not an initializer_list (unless the type of arg is std::initializer_list), however, this leaves us with d and h. Option d, the static_cast, will always give you the type you expect and can be used in almost any context. The one advantage of option h, however, is that

Construction of a non-temporary object

// Direct initialization
T a(arg);

auto b = T(arg);

T c{arg};

auto d = T{arg};

T e = arg;

T f({arg});

auto g = T({arg});

T h{{arg}};

auto i = T{{arg}};

T j = {arg};

More to come later...

Further reading

This problem was the subject of Guru of the Week #1.