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
.