When to use the brace-enclosed initializer?

C++C++11Initializer List

C++ Problem Overview


In C++11, we have that new syntax for initializing classes which gives us a big number of possibilities how to initialize variables.

{ // Example 1
  int b(1);
  int a{1};
  int c = 1;
  int d = {1};
}
{ // Example 2
  std::complex<double> b(3,4);
  std::complex<double> a{3,4};
  std::complex<double> c = {3,4};
  auto d = std::complex<double>(3,4);
  auto e = std::complex<double>{3,4};
}
{ // Example 3
  std::string a(3,'x');
  std::string b{3,'x'}; // oops
}
{ // Example 4
  std::function<int(int,int)> a(std::plus<int>());
  std::function<int(int,int)> b{std::plus<int>()};
}
{ // Example 5
  std::unique_ptr<int> a(new int(5));
  std::unique_ptr<int> b{new int(5)};
}
{ // Example 6
  std::locale::global(std::locale("")); // copied from 22.4.8.3
  std::locale::global(std::locale{""});
}
{ // Example 7
  std::default_random_engine a {}; // Stroustrup's FAQ
  std::default_random_engine b;
}
{ // Example 8
  duration<long> a = 5; // Stroustrup's FAQ too
  duration<long> b(5);
  duration<long> c {5};
}

For each variable I declare, I have to think which initializing syntax I should use and this slows my coding speed down. I'm sure that wasn't the intention of introducing the curly brackets.

When it comes to template code, changing the syntax can lead to different meanings, so going the right way is essential.

I wonder whether there is a universal guideline which syntax one should chose.

C++ Solutions


Solution 1 - C++

I think the following could be a good guideline:

  • If the (single) value you are initializing with is intended to be the exact value of the object, use copy (=) initialization (because then in case of error, you'll never accidentally invoke an explicit constructor, which generally interprets the provided value differently). In places where copy initialization is not available, see if brace initialization has the correct semantics, and if so, use that; otherwise use parenthesis initialization (if that is also not available, you're out of luck anyway).

  • If the values you are initializing with are a list of values to be stored in the object (like the elements of a vector/array, or real/imaginary part of a complex number), use curly braces initialization if available.

  • If the values you are initializing with are not values to be stored, but describe the intended value/state of the object, use parentheses. Examples are the size argument of a vector or the file name argument of an fstream.

Solution 2 - C++

I am pretty sure there will never be a universal guideline. My approach is to use always curly braces remembering that

  1. Initializer list constructors take precedence over other constructors
  2. All standard library containers and std::basic_string have initializer list constructors.
  3. Curly brace initialization does not allow narrowing conversions.

So round and curly braces are not interchangeable. But knowing where they differ allows me to use curly over round bracket initialization in most cases (some of the cases where I can't are currently compiler bugs).

Solution 3 - C++

Outside of generic code (i.e. templates), you can (and I do) use braces everywhere. One advantage is that it works everywhere, for instance even for in-class initialization:

struct foo {
    // Ok
    std::string a = { "foo" };

    // Also ok
    std::string b { "bar" };

    // Not possible
    std::string c("qux");

    // For completeness this is possible
    std::string d = "baz";
};

or for function arguments:

void foo(std::pair<int, double*>);
foo({ 42, nullptr });
// Not possible with parentheses without spelling out the type:
foo(std::pair<int, double*>(42, nullptr));

For variables I don't pay much attention between the T t = { init }; or T t { init }; styles, I find the difference to be minor and will at worst only result in a helpful compiler message about misusing an explicit constructor.

For types that accept std::initializer_list though obviously sometimes the non-std::initializer_list constructors are needed (the classical example being std::vector<int> twenty_answers(20, 42);). It's fine to not use braces then.


When it comes to generic code (i.e. in templates) that very last paragraph should have raised some warnings. Consider the following:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T { std::forward<Args>(args)... } }; }

Then auto p = make_unique<std::vector<T>>(20, T {}); creates a vector of size 2 if T is e.g. int, or a vector of size 20 if T is std::string. A very telltale sign that there is something very wrong going on here is that there's no trait that can save you here (e.g. with SFINAE): std::is_constructible is in terms of direct-initialization, whereas we're using brace-initialization which defers to direct-initialization if and only if there's no constructor taking std::initializer_list interfering. Similarly std::is_convertible is of no help.

I've investigated if it is in fact possible to hand-roll a trait that can fix that but I'm not overly optimistic about that. In any case I don't think we would be missing much, I think that the fact that make_unique<T>(foo, bar) result in a construction equivalent to T(foo, bar) is very much intuitive; especially given that make_unique<T>({ foo, bar }) is quite dissimilar and only makes sense if foo and bar have the same type.

Hence for generic code I only use braces for value initialization (e.g. T t {}; or T t = {};), which is very convenient and I think superior to the C++03 way T t = T();. Otherwise it's either direct initialization syntax (i.e. T t(a0, a1, a2);), or sometimes default construction (T t; stream >> t; being the only case where I use that I think).

That doesn't mean that all braces are bad though, consider the previous example with fixes:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{ return std::unique_ptr<T> { new T(std::forward<Args>(args)...) }; }

This still uses braces for constructing the std::unique_ptr<T>, even though the actual type depend on template parameter T.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionhelamiView Question on Stackoverflow
Solution 1 - C++celtschkView Answer on Stackoverflow
Solution 2 - C++juanchopanzaView Answer on Stackoverflow
Solution 3 - C++Luc DantonView Answer on Stackoverflow