Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 735
template boy Avatar asked Jan 12 '15 02:01

template boy


People also ask

What is uniform initialization?

Uniform initialization is a feature in C++ 11 that allows the usage of a consistent syntax to initialize variables and objects ranging from primitive type to aggregates. In other words, it introduces brace-initialization that uses braces ({}) to enclose initializer values.

Why do we initialize uniforms?

The uniform initialization is a feature that permits the usage of a consistent syntax to initialize variables and objects which are ranging from primitive type to aggregates. In other words, it introduces brace-initialization that applies braces ({}) to enclose initializer values.

What is direct initialization?

Direct Initialization or Assignment Operator (Syntax) This assigns the value of one object to another object both of which are already exists. Copy initialization is used when a new object is created with some existing object. This is used when we want to assign existing object to new object.

What is the order of initialization?

In Java, the order for initialization statements is as follows: static variables and static initializers in order. instance variables and instance initializers in order. constructors.


1 Answers

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)

like image 163
quantdev Avatar answered Oct 19 '22 11:10

quantdev