std::atomic ai1 {0 }; // fine
std::atomic ai2 (0 ); // fine
std::atomic ai3 =0; // error!
It's thus easy to understand why braced initialization is called “uniform.” Of C++'s three ways to designate an initializing expression, only braces can be used everywhere.
A novel feature of braced initialization is that it prohibits implicit narrowing conversions among built-in types. If the value of an expression in a braced initializer isn't guaranteed to be expressible by the type of the object being initialized, the code won't compile:
double x, y, z;
…
int sum1 {x + y + z }; // error! sum of doubles may
// not be expressible as int
Initialization using parentheses and “ =
” doesn't check for narrowing conversions, because that could break too much legacy code:
int sum2 (x + y + z ); // okay (value of expression
// truncated to an int)
int sum3 =x + y + z; // ditto
Another noteworthy characteristic of braced initialization is its immunity to C++'s most vexing parse . A side effect of C++'s rule that anything that can be parsed as a declaration must be interpreted as one, the most vexing parse most frequently afflicts developers when they want to default-construct an object, but inadvertently end up declaring a function instead. The root of the problem is that if you want to call a constructor with an argument, you can do it like this,
Widget w1 (10); // call Widget ctor with argument 10
but if you try to call a Widget constructor with zero arguments using the analogous syntax, you declare a function instead of an object:
Widget w2 (); // most vexing parse! declares a function
// named w2 that returns a Widget!
Functions can't be declared using braces for the parameter list, so default-constructing an object using braces doesn't have this problem:
Widget w3 {}; // calls Widget ctor with no args
There's thus a lot to be said for braced initialization. It's the syntax that can be used in the widest variety of contexts, it prevents implicit narrowing conversions, and it's immune to C++'s most vexing parse. A trifecta of goodness! So why isn't this Item entitled something like “Prefer braced initialization syntax”?
The drawback to braced initialization is the sometimes-surprising behavior that accompanies it. Such behavior grows out of the unusually tangled relationship among braced initializers, std::initializer_list
s, and constructor overload resolution. Their interactions can lead to code that seems like it should do one thing, but actually does another. For example, Item 2explains that when an auto
-declared variable has a braced initializer, the type deduced is std::initializer_list
, even though other ways of declaring a variable with the same initializer would yield a more intuitive type. As a result, the more you like auto, the less enthusiastic you're likely to be about braced initialization.
In constructor calls, parentheses and braces have the same meaning as long as std::initializer_list
parameters are not involved:
class Widget {
public:
Widget(int i, bool b); // ctors not declaring
Widget(int i, double d); // std::initializer_list params
…
};
Widget w1 (10, true ); // calls first ctor
Widget w2 {10, true }; // also calls first ctor
Widget w3 (10, 5.0 ); // calls second ctor
Widget w4 {10, 5.0 }; // also calls second ctor
If, however, one or more constructors declare a parameter of type std::initializer_list
, calls using the braced initialization syntax strongly prefer the overloads taking std::initializer_list
s. Strongly . 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. If the Widget class above is augmented with a constructor taking a std::initializer_list
, for example,
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
Widget(std::initializer_list il);// added
…
};
Widget
s w2
and w4
will be constructed using the new constructor, even though the type of the std::initializer_list
elements ( long double
) is, compared to the non- std::initializer_list
constructors, a worse match for both arguments! Look:
Widget w1 (10, true ); // uses parens and, as before,
// calls first ctor
Widget w2 {10, true }; // uses braces, but now calls
// std::initializer_list ctor
// (10 and true convert to long double)
Widget w3 (10, 5.0 ); // uses parens and, as before,
// calls second ctor
Widget w4 {10, 5.0 }; // uses braces, but now calls
// std::initializer_list ctor
// (10 and 5.0 convert to long double)
Even what would normally be copy and move construction can be hijacked by std::initializer_list
constructors:
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
Widget(std::initializer_list il); // as before
operator float() const; // convert
… // to float
};
Widget w5 (w4 ); // uses parens, calls copy ctor
Widget w6 {w4 }; // uses braces, calls
// std::initializer_list ctor
// (w4 converts to float, and float
// converts to long double)
Widget w7 (std::move(w4) ); // uses parens, calls move ctor
Widget w8 {std::move(w4) }; // uses braces, calls
// std::initializer_list ctor
// (for same reason as w6)
Compilers' determination to match braced initializers with constructors taking std::initializer_list
s is so strong, it prevails even if the best-match std::initializer_list
constructor can't be called. For example:
class Widget {
public:
Widget(int i, bool b); // as before
Widget(int i, double d); // as before
Widget(std::initializer_list< bool> il); // element type is
// now bool
… // no implicit
}; // conversion funcs
Читать дальше