Let's say I have a template that stores an object of type T
. I want to pass constructor arguments in order to initialize the data member. Should I use uniform-initialization or direct-initialization with non-curly braces?:
template<typename T>
struct X
{
template<typename... Args>
X(Args&&... args)
: t(std::forward<Args>(args)...) // ?
/* or */ : t{std::forward<Args>(args)...} // ?
private:
T t;
};
If the object I want to store is a std::vector
and I choose the curly-brace style (uniform-initialization) then the arguments I pass will be forwarded to the vector::vector(std::initializer_list<T>)
constructor, which may or may not be what I want.
On the other hand, if I use the non-curly brace style I loose the ability to add elements to the vector through its std::initializer_list
constructor.
What form of initialization should I use when I don't know the object I am storing and the arguments that will be passed in?
To be clear the ambiguity arises for types having multiple constructors, including one taking an std::initializer_list
, and another one whose parameters (when initialized with braces) may be interpreted as an std::initializer_list
by the compiler. That is the case, for instance, with std::vector<int>
:
template<typename T>
struct X1
{
template<typename... Args>
X1(Args&&... args)
: t(std::forward<Args>(args)...) {}
T t;
};
template<typename T>
struct X2
{
template<typename... Args>
X2(Args&&... args)
: t{std::forward<Args>(args)...} {}
T t;
};
int main() {
auto x1 = X1<std::vector<int>> { 42, 2 };
auto x2 = X2<std::vector<int>> { 42, 2 };
std::cout << "size of X1.t : " << x1.t.size()
<< "\nsize of X2.t : " << x2.t.size();
}
(Note that the only difference is braces in X2
members initializer list instead of parenthesis in X1
members initializer list)
Output :
size of X1.t : 42
size of X2.t : 2
Standard Library authors faced this real problem when writing utility templates such as std::make_unique
, std::make_shared
or std::optional<>
(that are supposed to perfectly forward for any type) : which initialization form is to be preferred ? It depends on client code.
There is no good answer, they usually go with parenthesis (ideally documenting the choice, so the caller knows what to expect). Idiomatic modern c++11 is to prefer braced initialization everywhere (it avoids narrowing conversions, avoid c++ most vexing parse, etc..)
A potential workaround for disambiguation is to use named tags, extensively discussed in this great article from Andrzej's C++ blog :
namespace std{
constexpr struct with_size_t{} with_size{};
constexpr struct with_value_t{} with_value{};
constexpr struct with_capacity_t{} with_capacity{};
}
// These contructors do not exist.
std::vector<int> v1(std::with_size, 10, std::with_value, 6);
std::vector<int> v2{std::with_size, 10, std::with_value, 6};
This is verbose, and apply only if you can modify the ambiguous type(s) (e.g. types that expose constructors taking an std::initializer_list
and other constructors whose arguments list maybe converted to an std::initializer list
)