使用移动成本低但复制成本高的对象初始化容器的首选方法是什么

What is the preferred way to initialize a container with objects that are cheap to move but heavy to copy

考虑以下代码:

#include <iostream>
#include <vector>

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
  C(C&&) {std::cout << "A move was made.\n";}
};

std::vector<C> g() {
  std::vector<C> ret {C(), C(), C()};
  return ret;
}

std::vector<C> h() {
  std::vector<C> ret;
  ret.reserve(3);
  ret.push_back(C());
  ret.push_back(C());
  ret.push_back(C());
  return ret;
}

int main() {
  std::cout << "Test g\n";
  std::vector<C> v1 = g();

  std::cout << "Test h\n";
  std::vector<C> v2 = h();
}

g++ -std=c++11 main.cpp && ./a.out编译,结果为:

Test g
A copy was made.
A copy was made.
A copy was made.
Test h
A move was made.
A move was made.
A move was made.

请注意,这两个函数都使用复制省略,因此返回的 std::vector<C> 不会被复制。

我明白为什么 h() 使用 move-constructor,但为什么 g() 使用 copy-constructor

来自 vector's doc:

(6) initializer list constructor

Constructs a container with a copy of each of the elements in il, in the same order.

看起来 initializer-list 总是 copy 元素,那么这可能意味着如果 C 很便宜,initializer-list constructor 的性能可能会受到影响移动但复制很重。

所以我的问题是:用移动成本低但复制重的对象初始化容器(例如 vector)的首选方法是什么?

您可以使用一些样板从初始化列表中移动。

template<class T>
struct force_move{
  mutable T t;

  template<class...Args>
  force_move(Args&&...args):
    t(std::forward<Args>(args)...)
  {}
  // todo: code that smartly uses {} if () does not work?

  force_move()=default;
  force_move(force_move const&)=delete;

  template<class U, class...Args>
  force_move(std::initializer_list<U> il, Args&&...args):
    t(il, std::forward<Args>(args)...)
  {}

  operator T()const{ return std::move(t); }
};

template<class T>
struct make_container {
  std::initializer_list<force_move<T>> il;
  make_container( std::initializer_list<force_move<T>> l ):il(l) {}

  template<class C>
  operator C()&&{
    return {il.begin(), il.end()};
  }
};

使用:

std::vector<C> v=make_container<C>{ {}, {} };

简洁高效,解决了你的问题

(可能应该是上面的 operator T&&。不确定,而且我对返回右值引用持怀疑态度...)

现在,这似乎有点乱七八糟。但是,替代方案很糟糕。

手动推送 back/emplace 返回列表很丑陋,并且在您添加储备要求以实现最大效率后变得更丑陋。而且幼稚的il解决方案无法移动。

不让您在声明实例的地方列出元素的解决方案很尴尬,在我看来。您希望能够将内容列表放在声明旁边。

另一种 "local list" 替代方法是创建一个内部初始化 std::array (可能是 ref 包装器)的可变函数,然后从该数组移动到容器中。但是,这不允许 { {}, {}, {} } 样式列表,所以我发现它缺少。

我们可以这样做:

template<class T, std::size_t N>
std::vector<T> move_from_array( T(&arr)[N] ){
  return {std::make_move_iterator(std::begin(arr)), std::make_move_iterator(std::end(arr))};
}

然后:

C arr[]={{}, {}, {}};
std::vector<C> v = move_from_array(arr);

唯一的缺点是在使用时需要两个声明。但是代码没有我的第一个解决方案那么钝。

可惜std::initializer_listdoesn't really work with move semantics.

如果您需要向构造函数提供参数,我会改用 emplace_back,它会就地构造元素:

std::vector<C> h() {
  std::vector<C> ret;
  ret.reserve(3);
  ret.emplace_back(arg1);
  ret.emplace_back(arg2,arg3);
  ret.emplace_back(0,12);
  return ret;
}

cannot move from an initializer_list (barring gymnastics with mutable as in Yakk's answer) because the elements of an initializer_list are declared const (moving elements of an initialization_list considered dangerous?).

我建议在具有聚合初始化的容器中构造对象,即经典数组或 std::array,然后从 move iterators:

构造向量
std::vector<C> h() {
    C[] arr{C(), C(), C()};
    return std::vector<C>(
        std::make_move_iterator(std::begin(arr)),
        std::make_move_iterator(std::end(arr)));
}