【问题标题】:How to design classes with constructor taking a std::initializer_list?如何使用带有 std::initializer_list 的构造函数设计类?
【发布时间】:2015-04-07 01:49:14
【问题描述】:

当类的构造函数重载采用std::initializer_list 时,即使其他构造函数重载看起来更匹配,这个重载也会优先。这个问题在 Sutter 的 GotW#1,第 2 部分以及 Meyers 的 Effective Modern C++,第 7 项中有详细描述。

这个问题的典型表现是在大括号初始化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 都建议避免使用 initializer_list 构造函数重载可能导致程序员产生歧义的类设计。

萨特:

准则:当你设计一个类时,避免提供一个构造函数 用 initializer_list 构造函数模棱两可地重载,因此 用户不需要使用 () 来访问这样一个隐藏的构造函数。

迈耶斯:

因此,最好设计您的构造函数,以便 调用的重载不受客户端是否使用括号或 大括号。换句话说,从现在被视为错误的东西中学习 std::vector 接口的设计,并将您的类设计为 避免它。

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

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

【问题讨论】:

    标签: c++ c++11 vector initializer-list


    【解决方案1】:

    为了完整起见,避免歧义的一种可能方法(而不是我提倡的方法)是使用静态工厂方法作为将 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}
    

    【讨论】:

    • 这种方法的缺点是“静态化”重载不能用于初始化堆上的容器。
    • 我的意思是说“静态化”重载不能用于直接初始化堆分配的容器。我总是可以这样做:auto heapContainer = new Container(Container::with(1,2));(这可能会导致轻微的性能损失)。
    【解决方案2】:

    我将采用与标准在pair 中使用piecewise_construct 或在unique_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 的元素,所以标签有助于澄清这一点。

    【讨论】:

    • 酷!我不知道他们已经在标准库的其他部分解决了这个问题。
    • 这种方法的优点是构造函数重载可用于堆和堆栈分配。
    • @EmileCormier:无论哪种方式,您都可以将这些重载用于堆和堆栈分配......不是吗?我想不出有什么能阻止它。
    • @MooingDuck:我认为你是对的。但是,对于我提出的解决方案,它将涉及调用复制/移动构造函数。而使用 Barry 的解决方案,容器将直接从构造函数参数初始化。
    • 是否有理由将标签放在前面? (也许,为了与具有参数包的函数保持一致?)如果没有,为什么不vector(size_type, copies_of_t, T const&amp;);? (顺便说一句,C++11 删除了 const value_type&amp; 的默认参数,可能是因为您可以编写一个值初始化 N 个元素而不需要可复制性的 ctor。)
    猜你喜欢
    • 1970-01-01
    • 2014-09-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-31
    • 2015-01-31
    • 1970-01-01
    相关资源
    最近更新 更多