c++

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

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) (eg types that expose constructors taking an std::initializer_list and other constructors whose arguments list maybe converted to an std::initializer list )


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.

    链接地址: http://www.djcxy.com/p/83016.html

    上一篇: 如何使用Rust编写本地Mac OS X GUI?

    下一篇: C ++