std::vector 的高效直接初始化

Efficient direct initialization of a std::vector

我有一个结构,比如说

struct A {
  A(int n) : n(n) {}
  int n;
};

我想用一些元素初始化一个 std::vector。我可以通过使用初始化列表或放置新元素来做到这一点:

// 1st version: 3 ctors, 3 copy ctors, 3 dtors                                           
std::vector<A> v1{1, 2, 3};

// 2nd version: 3 ctors                                                                  
std::vector<A> v2;
v2.reserve(3);
v2.emplace_back(4);
v2.emplace_back(5);
v2.emplace_back(6);

如评论所示,第一个版本调用了 3 个构造函数、3 个复制构造函数和 3 个析构函数。带 emplace 的版本只使用了 3 个构造函数。

问题: 显然第二个版本更好,但是第一个版本更简洁。我可以两全其美吗?我可以直接初始化而不需要额外费用吗?

(这里是 A 结构的 longer version,它显示了正在发生的事情。)

由于 A 可从 int 转换而来,您可以使用 vector 的范围构造函数:

auto inits = {1, 2, 3};
std::vector<A> v1{std::begin(inits), std::end(inits)};

或者在单个声明语句中(假设您可以依赖 RVO):

auto v1 = [inits={1, 2, 3}] { return std::vector<A>{std::begin(inits), std::end(inits)}; }();

扩展@ecatmur 的回答,我开发了一段代码,允许对任何类型的向量和任何构造函数调用提供非常通用的解决方案。向量的每个元素的构造函数参数存储在 tuple&&& 中,视情况而定),然后在构建元素时完美转发。每个元素只构造一次,本质上等同于emplace_back。此转发甚至允许构建仅移动类型的向量,例如 unique_ptr<?>.

(Update,由于 RVO,它应该简单地构造它们。然而不幸的是,元素类型确实需要至少一个复制构造函数或移动构造函数可见,即使优化器实际上跳过了它们。这意味着您可以构建 unique_ptr 的向量,但不能构建 mutex 的向量。)

auto v2 = make_vector_efficiently<A>(
         pack_for_later(1)        // 1-arg constructor of A
        ,pack_for_later(2,"two")  // 2-arg constructor of A
        ,pack_for_later(3)        // 1-arg constructor of A
        );

以上代码将创建一个包含三个元素的 vector<A>。在我的示例中,A 有两个构造函数,其中一个以 int,string 作为参数。

pack_for_later 构建一个 tuple 将其参数存储为 &/&& 引用。然后将其转换为对象(类型为 UniquePointerThatConverts,具有所需的转换运算符,在本例中为 operator A().

make_vector_efficiently 中构建了这些转换器对象的初始化列表,然后 vector 使用 begin()end() 的 initializer_list。您可能期望这些迭代器需要具有类型 T* 才能构造 vector<T>,但迭代器指向的类型可以 convert[=89= 就足够了] 到 T.

构造函数然后使用放置 new 从转换后的对象(复制)构造。但是,感谢 RVO,复制不会发生,转换器将有效地为我们做相当于 emplace_back 的工作。

无论如何,任何反馈表示赞赏。最后,将其扩展到 vector.

以外的其他容器是微不足道的

Full code on Coliru 应该适用于任何 C++11 编译器。


一些更详细的注释,以及重要功能的副本:

pack_for_later 只是构建一个 std::tuple。标准 make_tuple 不够好,因为它忽略了引用。 pack_for_later 构建的元组的每个元素都是一个引用(&&& 视情况而定,具体取决于原始参数是左值还是右值)

template<typename ...T> 
std:: tuple<T&&...> pack_for_later(T&&... args) { 
        // this function is really just a more
        // 'honest' make_tuple - i.e. without any decay
    return std:: tuple<T&&...> (std::forward<T>(args)...);
} 

接下来,make_vector_efficiently就是把所有的东西都放在一起的功能。它的第一个参数是 'Target' 类型,即我们希望创建的向量中元素的类型。 tuples 的集合被转换为我们的特殊转换器类型 UniquePointerThatConverts<Target> 并且向量的构造如上所述。

template<typename Target, typename ...PackOfTuples>
auto make_vector_efficiently(PackOfTuples&&... args)
    -> std::vector<Target>
{
    auto inits = { UniquePointerThatConverts<Target>(std::forward<PackOfTuples>(args))...};
    return std::vector<Target> {std::begin(inits), std::end(inits)};
}

因为 A 可以有多个构造函数,我们希望能够使用它们中的任何一个,所以 pack_for_later 可以 return 许多不同的类型(不要忘记左值和右值也是如此)。但是我们需要一个单一的类型来构建初始化列表。因此,我们定义一个合适的接口:

template<typename Target>
struct ConvInterface {
    virtual Target convert_to_target_type() const = 0;
    virtual ~ConvInterface() {}
};

每个元组因此被make_Conv_from_tuple转换为一个实现该接口的对象。它实际上 return 是这样一个对象的 unique_ptr,然后存储在具有实际转换运算符的 UniquePointerThatConverts 中。正是这种类型存储在用于初始化向量的初始化列表中。

template<typename Target>
struct UniquePointerThatConverts {
    std:: unique_ptr<ConvInterface<Target>> p; // A pointer to an object
               // that implements the desired interface, i.e.
               // something that can convert to the desired
               // type (Target).

    template<typename Tuple>
    UniquePointerThatConverts(Tuple&& p_)
    : p ( make_Conv_from_tuple<Target>(std:: move(p_)) )
    {
        //cout << __PRETTY_FUNCTION__ << endl;
    }
    operator Target () const {
        return p->convert_to_target_type();
    }
};

当然还有从包中构造的实际转换运算符。

template<typename Target, typename ...T>
struct Conv : public ConvInterface<Target> {
    std::tuple<T...> the_pack_of_constructor_args;
    Conv(std::tuple<T...> &&t) : the_pack_of_constructor_args(std:: move(t)) {}

    Target convert_to_target_type () const override {
        using idx = typename make_my_index_sequence<sizeof...(T)> :: type;
        return foo(idx{});
    }
    template<size_t ...i>
    Target foo(my_index_sequence<i...>) const {
        // This next line is the main line, constructs
        // something of the Target type (see the return
        // type here) by expanding the tuple.
        return {
                std:: forward
                    < typename std:: tuple_element < i , std::tuple<T...> > :: type >
                    (std:: get<i>(the_pack_of_constructor_args))
            ...
        };
    }
};