c++c++11initialization

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?


Solution

  • 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)