将一个向量附加到另一个向量时,为什么移动元素比复制它们便宜?

When appending one vector to another, why is moving the elements any cheaper than copying them?

本文关键字:向量 复制 移动 为什么 元素 一个 另一个      更新时间:2023-10-16

假设我有两个向量,srcdst,我想将src附加到dst的末尾。

我注意到关于此任务的大多数答案都建议这样做:

dst.insert(dst.end(),
std::make_move_iterator(src.begin()),
std::make_move_iterator(src.end()));

在此:

dst.insert(dst.end(), src.begin(), src.end());

据我所知,在这两种情况下,将元素推送(插入)到向量都需要在向量末尾为插入的元素分配空间以确保内存连续性,并且我假设在这种情况下copymove成本是相同的。

移动物体会使它们立即被摧毁,这是这样做的唯一好处,还是我缺少其他东西?

编辑:你能解释一下在这两种情况下:

  1. 向量包含纯数据,例如:int。
  2. 向量包含类对象。

事实上,如果复制和移动元素的成本相同(在您的示例中是int类型的元素),那么就没有区别了。

移动只对本身将其数据存储在堆上的元素产生影响,即使用分配的内存(例如,如果元素std::stringstd::vector<something>)。在这种情况下,移动或复制元素会产生(潜在的巨大)差异(前提是移动构造函数和operator=(value_type&&)正确实现/启用),因为移动只是复制指向分配内存的指针,而副本是深的:它分配新内存并复制所有数据,包括递归深层副本(如果适用)。

至于与存储在std::vector中的数据相关的成本,如果附加的元素超出容量,则会产生一些成本。在这种情况下,将调整整个矢量的大小,包括移动其所有元素。这样做的原因是std::vector,根据规范,将其所有元素存储在单个数组中。如果追加容器是代码中频繁的操作,则可能需要考虑其他容器,例如std::liststd::deque

我假设复制和移动成本是相同的。

你假设错了。

这与向量或插入无关。这与构造函数之间的相对成本差异有关

ClassT::ClassT(const ClassT& orig);

ClassT::ClassT(ClassT&& orig);

总能找到比复制构造函数更便宜或相等的移动构造函数。

构造

函数在采用右值时称为"移动构造函数" 作为参数引用。它没有义务移动任何东西, 类不需要具有要移动的资源和"移动" 构造函数'可能无法像允许的那样移动资源 (但可能不明智)参数是常量右值的情况 参考(常量 T&&)。

例如,ClassT可以执行与复制构造函数完全相同的操作。或者ClassT可以执行更好的移动结构。这意味着如果在 dst 中插入时不需要 src 中对象的原始实例,则应使用移动操作。

你已经有了答案。但我认为一个简短的程序可以很好地说明它:

#include <iostream>
#include <utility>
#include <vector>
#include <iterator>
struct expansive {
static unsigned instances;
static unsigned copies;
static unsigned assignments;
expansive() { ++instances; }
expansive(expansive const&) { ++copies; ++instances; }
expansive& operator=(expansive const&) { ++assignments; return *this; }
};
unsigned expansive::instances = 0;
unsigned expansive::copies = 0;
unsigned expansive::assignments = 0;
struct handle {
expansive *h;
handle() : h(new expansive) { }
~handle() { delete h; }
handle(handle const& other) : h(new expansive(*other.h)) { }
handle(handle&& other) : h(other.h) { other.h = nullptr; }
handle& operator=(handle const& other) { *h = *other.h; return *this; }
handle& operator=(handle&& other)  { std::swap(h, other.h); return *this; }
};
int main() {
{
std::vector<handle> v1(10), v2(10);
v1.insert(end(v1), begin(v2), end(v2));
std::cout << "When copying there were "
<< expansive::instances   << " instances of the object with " 
<< expansive::copies      << " copies and "
<< expansive::assignments << " assignments made." << std::endl;
}
expansive::instances = expansive::copies = expansive::assignments = 0;
{
std::vector<handle> v1(10), v2(10);
v1.insert(end(v1), std::make_move_iterator(begin(v2)),
std::make_move_iterator(end(v2)));

std::cout << "When moving there were "
<< expansive::instances   << " instances of the object with " 
<< expansive::copies      << " copies and "
<< expansive::assignments << " assignments made.n";
}
return 0;
}

expansive对复制成本非常高的资源进行建模(想象一下打开文件句柄、网络连接等)。这是handle管理的资源。为了保持程序的正确性,handle在复制本身时仍必须执行昂贵的复制。

现在,当程序运行时,它会生成以下输出:

复制时,对象有 30 个实例,其中 10 个副本和 完成 0 次作业。 移动时,有 20 个实例 对象,0 个副本和 0 个分配。

什么意思?这意味着,如果我们只想将句柄转移到另一个容器,我们必须在此过程中做一些非常广泛的工作(我们尝试传输的资源数量呈线性关系)。移动语义在这里拯救我们。
通过使用move_iterator,实际昂贵的资源被直接转移,而不是被多余的复制。这可以转化为性能的大幅提升。