This page looks best with JavaScript enabled

[EMCpp]Item-7 Distinguish Betwenn () and {} When Creating Objects

 ·  ☕ 4 min read · 👀... views

Braced initialization is the most widely usable initialization syntax, which can also prevent narrowing conversions, as well as being immune to C++’s most vexing parse.

The good part

Syntacticly widest usage

Syntax choices for object initialization in C++11 are confusing to support 3 forms: parentheses, equal signs, and braces.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int x(0);   // initializer with parentheses
int y = 0;  // initializer with "="
int z{ 0 }; // initializer with braces
int zz = {0}; // initializer with "=" and braces, treated the same as braces-only version.

Widget w1;  // default ctor.
Widget w2 = w1; // copy ctor
w1 = w2;  // assignment, calls copy operator=

// easy to specify the init. content of a container
std::vector<int> v{ 1, 3, 5 };  // v's init. content is 1, 3, 5

// specify default init. value for non-static data members, where parentheses not allowed
class Widget {
    ...
private:
    int x{ 0 }; // fine
    int y = 0;  // fine
    int z(0);   // error
};

// specify init. value for uncopyable objects (EMCpp item 40), where "=" not allowed
std::atomic<int> ai1{ 0 }; // fine
std::atomic<int> ai2(0);   // fine
std::atomic<int> ai3 = 0;  // error

From the example above, it’s easy to see why braced initialization is called “uniform” - a single initialization syntax that aims to be used anywhere and express everything.

Preventing narrowing conversion

Braced initialization prohibits implicit narrowing conversions among built-in types:

1
2
3
4
5
double x, y, z;
...
int sum1{ x + y + z }; // error
int sum2(x + y + z);  // okay to truncate value of expression to an int
int sum3 = x + y + z; // ditto

Immune to most vexing parse

Most vexing parse: anything that can be parsed as a declaration must be interpreted as one. This may be annoying when developers want to default-construct an object, but inadvertently end up declaring a function instead. Using braces, we don’t have such an issue:

1
2
3
Widget w1(10); // call Widget ctor with arg. 10
Widget w2();  // intend to call a Widget const. with zero arg., end up declaring a func. named w2 that returns a Widget
Widget w3{}; // calls Widget ctor with no args.

The not-so-good part

Unintuitive behavior with auto

As EMCpp item 2 explains, when an auto-declared variable has a braced initializer, the type deduced is std::initializer_list, which may lead to surprising behaviors sometimes.

(Too) high overloading priority

Calls using braced initialization syntax strongly prefer the overloads taking std::initializer_lists: if there’s any way for compilers to construe a call using a braced initializer to be to a constructor taking a std::initializer_list, compilers will employ that interpretation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Widget {
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<long double> il);
    operator float() const; // convert to float
    ...
};

Widget w1(10, true); // calls first ctor.
Widget w2{10, true}; // calls std::initializer_list ctor, 10 and true convert to long double
Widget w3(10, 5.6); // calls second ctor
Widget w4{10, 5.6}; // calls std::initializer_list ctor, 10 and 5.6 convert to long double
Widget w5(w4); // calls copy ctor
Widget w6{w4}; // calls std::initializer_list ctor, w4 converts to float, and float converts to long double
Widget w7(std::move(w4)); // calls move ctor
Widget w8{std::move(w4)}; // calls std::initializer_list ctor, same conversion as w6

The priority is so high that it prevails even if the best-match std-initializer_list constructor can’t be called:

1
2
3
4
5
6
7
8
9
class Widget {
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<bool> il); // no implicit conversion funcs
    ...
};

Widget w{10, 5.0}; // error: requires narrowing conversions. The other callable ctors is shadowed

Only if there’s no way to convert the types of the arguments in a braced initializer to the type in a std::initializer_list do compilers fall back on normal overload resolution:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Widget {
public:
    Widget(int i, bool b);
    Widget(int i, double d);
    Widget(std::initializer_list<std::string> il); // no implicit conversion funcs
    ...
};

Widget w1(10, true);  // calls first ctor
Widget w2{10, true};  // calls first ctor
Widget w3(10, 5.0);  // calls second ctor
Widget w3{10, 5.0};  // calls second ctor

Edge case: Empty braces mean no arguments, not an empty std::initializer_list. For example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Widget {
public:
    Widget();
    Widget(std::initializer_list<int> il); // no implicit conversion funcs
    ...
};

Widget w1; // calls default ctor
Widget w2(); // calls default ctor
Widget w3{}; // calls default ctor
Widget w4({}); // calls std::initializer_list ctor with empty list
Share on
Support the author with