利用容器的移动语义和元素初始化

Utilizing move-semantics and element-wise initialization of containers

经常看到使用 STL 算法的示例用 list-initialized 容器来说明,例如:

std::vector< int > v{1, 2, 3, 4};

但是当这种方法用于(重量级)(不像ints)时,它意味着过度的复制操作,即使他们由 rvalue 传递( 移至 ),因为上例中使用的 std::initializer_list 仅提供 const_iterators.

为了解决这个问题,我使用以下 (C++17) 方法:

template< typename Container, typename ...Args >
Container make_container(Args &&... args)
{
    Container c;
    (c.push_back(std::forward< Args >(args)), ...);
    // ((c.insert(std::cend(c), std::forward< Args >(args)), void(0)), ...); // more generic approach
    return c;
}

auto u = make_container< std::vector< A > >(A{}, A{}, A{});

但是当我执行以下操作时,它变得不令人满意:

A a;
B b;
using P = std::pair< A, B >;
auto v = make_container< std::vector< P > >(P{a, b}, P{std::move(a), std::move(b)});

这里我想通过移动操作替换复制操作来为每个值保存一个复制操作(假设移动 AB 比复制便宜得多) , 但通常不能,因为函数参数的求值顺序在 C++ 中未定义。我目前的解决方案是:

template< Container >
struct make_container
{

    template< typename ...Args >
    make_container(Args &&... args)
    {
        (c.push_back(std::forward< Args >(args)), ...);
    }

    operator Container () && { return std::move(c); }

private :

    Container c;

};

A a; B b;
using P = std::pair< A, B >;
using V = std::vector< P >;
V w = make_container< V >{P{a, b}, P{std::move(a), std::move(b)}};

在构造函数体中做一些重要的工作通常被认为是一种不好的做法,但在这里我大量使用了 list-initialization[=38= 的属性 ] — 事实上它是严格从左到右排序的。

从某些特定的角度来看,这是完全错误的做法吗?除了上述方法之外,这种方法还有哪些缺点?目前是否有另一种技术可以实现函数参数求值的可预测顺序(在 C++11、C++14、C++1z 中)?

Is it totally wrong approach from some particular point of view?

如果被调用的函数在复制之前结束移动,那么理解起来会不必要地困难并且会严重中断。请记住,std::move 不会移动。它启用 移动。而已。最终移动的是被调用的函数。如果 从左到右处理参数,它将起作用。没有就没有。

说清楚是怎么回事。

A a; A b;
using V = std::vector< A >;
A c {a};
V v = make_container< V >{std::move(a), b, std::move(c)};

有更好的解决方案:

template<class Container, std::size_t N>
inline Container make_container(typename Container::value_type (&&a)[N])
{
    return Container(std::make_move_iterator(std::begin(a)), std::make_move_iterator(std::end(a)));
}

你可以这样使用它:

make_container<std::vector<A>>({A(1), A(2)})

它不需要可变参数模板,它仍然是列表初始化但不是 std::initializer_list,这次它是一个普通数组,因此您可以从中移动元素。

与您的原始解决方案相比的显着优势:

  • 更好的性能:它直接调用Container的ctor可以提供更好的性能(例如std::vector可以保留所有需要的内存)
  • Eval顺序保证:是列表初始化

DEMO