template boy
template boy

Reputation: 10480

Uniform- or direct-initialization when initializing?

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?

Upvotes: 12

Views: 6533

Answers (3)

Javier
Javier

Reputation: 1

A minor improvement:

direct initialization:

T t(/*args*/);

list initialization:

direct-list-initialization ({}):

T t{*list of values*};

copy-list-initialization (= {}):

T t =  {*list of values*}

The official names are here: https://en.cppreference.com/w/cpp/language/list_initialization

A poster incorrectly claims that the official name is brace-initialization.

In the case of an std::vector, it makes a difference if you use direct initialization vs direct-list-initialization:

std::vector<int> v1(42); // vector with 42 elements, all zeros
std::vector<int> v2{42}; // vector with 1 element with value 42

std::vector<int> v3{1,2,3};   // direct-list-initialization
std::vector<int> v4= {1,2,3}; // copy-list-initialization

Upvotes: 0

quantdev
quantdev

Reputation: 23793

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

Demo


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)

Upvotes: 4

Potatoswatter
Potatoswatter

Reputation: 137770

As with any initialization,

  • Use braces when the object contains a value, or several values which are getting piecewise initialized. This includes aggregate classes and numbers.

    Braces appear more like a list of items.

  • Use parentheses when the object's initial state is computed from parameters.

    Parentheses appear more like a sequence of function arguments.

This general rule includes the case of a container like std::vector<int> which may be initialized with N copies of a number (std::vector<int>(4,5)) or a pair of numbers (std::vector<int>{4,5}).

By the way, since "uniform" initialization isn't really a catch-all, that term is discouraged. The official name is brace-initialization.

Upvotes: 1

Related Questions