如何设计类,构造函数采用std::initializer_list?

How to design classes with constructor taking a std::initializer_list?

当 classes 有一个构造函数重载采用 std::initializer_list 时,即使其他构造函数重载看起来更匹配,这个重载也会优先。 Sutter 的 GotW#1, part 2, as well as Meyers' Effective Modern C++,第 7 项

中详细描述了此问题

这个问题表现出来的 classic 例子是大括号初始化 std::vector:

std::vector<int> vec{1, 2};
// Is this a vector with elements {1, 2}, or a vector with a single element 2?

Sutter 和 Meyers 都建议避免 class 设计,其中 initializer_list 构造函数重载可能会给程序员带来歧义。

萨特:

Guideline: When you design a class, avoid providing a constructor that ambiguously overloads with an initializer_list constructor, so that users won’t need to use ( ) to reach such a hidden constructor.

迈耶斯:

As a result, it’s best to design your constructors so that the overload called isn’t affected by whether clients use parentheses or braces. In other words, learn from what is now viewed as an error in the design of the std::vector interface, and design your classes to avoid it.

但是他们都没有描述 vector 应该如何设计来避免这个问题!

所以这是我的问题:应该如何设计 vector 以避免与 initializer_list 构造函数重载的歧义(不丢失任何功能)?

为了完整起见,避免歧义的一种可能方法(而不是我提倡的方法)是使用静态工厂方法作为将 initializer_list 构造函数与其他构造函数隔离开来的方法。

例如:

template <typename T>
class Container
{
public:
    static Container with(size_t count, const T& value)
    {
        return Container(Tag{}, count, value);
    }

    Container(std::initializer_list<T> list) {/*...*/}

private:
    struct Tag{};
    Container(Tag, size_t count, const T& value) {/*...*/}
};

用法:

auto c1 = Container<int>::with(1, 2); // Container with the single element '2'
auto c2 = Container<int>{1, 2}; // Container with the elements {1, 2}

这种静态工厂方法让人想起对象 allocated and initialized in Objective-C。嵌套的 Tag 结构用于确保 initializer_list 重载不可行。


或者,initializer_list 构造函数可以更改为静态工厂方法,这样您就可以保持其他构造函数重载不变:

template <typename T>
class Container
{
public:
    static Container with(std::initializer_list<T> list)
    {
        return Container(Tag{}, list);
    }

    Container(size_t count, const T& value) {/*...*/}

private:
    struct Tag{};
    Container(Tag, std::initializer_list<T> list) {/*...*/}
};

用法:

auto c1 = Container<int>{1, 2}; // Container with the single element '2'
auto c2 = Container<int>::with({1, 2}); // Container with the elements {1, 2}

我将采用与标准在 pair 中的 piecewise_constructunique_lock 中的 defer_lock 相同的方法:在构造函数上使用标签:

struct n_copies_of_t { };
constexpr n_copies_of_t n_copies_of{};

template <typename T, typename A = std::allocator<T>>
class vector {
public:
    vector(std::initializer_list<T>);
    vector(n_copies_of_t, size_type, const T& = T(), const A& = A());
    // etc.
};

这样:

std::vector<int> v{10, 20}; // vector of 2 elems
std::vector<int> v2(10, 20); // error - not a valid ctor
std::vector<int> v3(n_copies_of, 10, 20); // 10 elements, all with value 20.

另外,我总是忘记它是 10 个值 20 的元素还是 20 个值 10 的元素,所以标签有助于澄清这一点。