为什么复制而不是移动数据元素?
Why are my data elements being copied instead of moved?
我正在执行一些关于移动语义的测试,我的类行为对我来说似乎很奇怪。
给定模拟类VecOfInt
:
class VecOfInt {
public:
VecOfInt(size_t num) : m_size(num), m_data(new int[m_size]) {}
~VecOfInt() { delete[] m_data; }
VecOfInt(VecOfInt const& other) : m_size(other.m_size), m_data(new int[m_size]) {
std::cout << "copy..." <<std::endl;
std::copy(other.m_data, other.m_data + m_size, m_data);
}
VecOfInt(VecOfInt&& other) : m_size(other.m_size) {
std::cout << "move..." << std::endl;
m_data = other.m_data;
other.m_data = nullptr;
}
VecOfInt& operator=(VecOfInt const& other) {
std::cout << "copy assignment..." << std::endl;
m_size = other.m_size;
delete m_data;
m_data = nullptr;
m_data = new int[m_size];
m_data = other.m_data;
return *this;
}
VecOfInt& operator=(VecOfInt&& other) {
std::cout << "move assignment..." << std::endl;
m_size = other.m_size;
m_data = other.m_data;
other.m_data = nullptr;
return *this;
}
private:
size_t m_size;
int* m_data;
};
好的案例
当我就地插入单个值时:
int main() { std::vector<VecOfInt> v; v.push_back(10); return 0; }
然后它给了我以下输出(我认为很好):
move...
奇怪的案例
当我就地插入三个不同的值时:
int main() { std::vector<VecOfInt> v; v.push_back(10); v.push_back(20); v.push_back(30); return 0; }
然后输出调用复制构造函数 3 次:
move... move... copy... move... copy... copy...
我在这里错过了什么?
std::vector
在重新分配时不使用移动构造和移动分配,除非它们noexcept
或没有复制替代方法。这是添加了noexcept
的示例:
class VecOfInt {
public:
VecOfInt(size_t num) : m_size(num), m_data(new int[m_size]) {}
~VecOfInt() { delete[] m_data; }
VecOfInt(VecOfInt const& other) : m_size(other.m_size), m_data(new int[m_size]) {
std::cout << "copy..." <<std::endl;
std::copy(other.m_data, other.m_data + m_size, m_data);
}
VecOfInt(VecOfInt&& other) noexcept : m_size(other.m_size) {
std::cout << "move..." << std::endl;
m_data = other.m_data;
other.m_data = nullptr;
}
VecOfInt& operator=(VecOfInt const& other) {
std::cout << "copy assignment..." << std::endl;
m_size = other.m_size;
delete m_data;
m_data = nullptr;
m_data = new int[m_size];
m_data = other.m_data;
return *this;
}
VecOfInt& operator=(VecOfInt&& other) noexcept {
std::cout << "move assignment..." << std::endl;
m_size = other.m_size;
m_data = other.m_data;
other.m_data = nullptr;
return *this;
}
private:
size_t m_size;
int* m_data;
};
实时示例输出:
move...
move...
move...
move...
move...
move...
这样做是为了保持异常安全。当调整std::vector
大小失败时,它将尝试将矢量保留为尝试之前的状态。但是,如果移动操作在重新分配过程中抛出一半,则没有安全的方法可以撤消已成功进行的移动。他们很可能也会扔。最安全的解决方案是复制移动可能会抛出。
std::vector
为其元素分配一个连续的内存块。当分配的内存太短而无法存储新元素时,将分配一个新块,并将所有当前元素从旧块复制到新块。
您可以使用std::vector::reserve()
在添加新元素之前预先调整std::vector
内存的容量。
请尝试以下操作:
int main() {
std::vector<VecOfInt> v;
v.reserve(3);
v.push_back(10);
v.push_back(20);
v.push_back(30);
return 0;
}
您将获得:
move...
move...
move...
但是要让移动构造函数即使在重新分配时也被调用,你应该让它noexcept
如下:
VecOfInt(VecOfInt&& other) noexcept {...}
tl;DR:如果你的移动构造函数不是noexcept
,std::vector
会复制而不是移动。
1. 这与成员和动态分配无关
问题不在于你如何处理 foo 领域。所以你的来源可能只是:
class foo {
public:
foo(size_t num) {}
~foo() = default
foo(foo const& other) {
std::cout << "copy..." <<std::endl;
}
foo(foo&& other) {
std::cout << "move..." << std::endl;
}
foo& operator=(foo const& other) {
std::cout << "copy assignment..." << std::endl;
return *this;
}
foo& operator=(foo&& other) {
std::cout << "move assignment..." << std::endl;
return *this;
}
};
你仍然得到相同的行为:尝试一下。
2.你看到的举动会分散注意力
现在,push_back()
将首先构造一个元素 - 在这种情况下foo
;然后确保向量中有空间;然后std::move()
它的位置。所以你的3个动作都是这样的。让我们尝试改用emplace_back()
,它在其位置构造向量元素:
#include <vector>
#include <iostream>
struct foo { // same as above */ };
int main() {
std::vector<foo> v;
v.emplace_back(10);
v.emplace_back(20);
v.emplace_back(30);
return 0;
}
这为我们提供了:
copy
copy
copy
试试吧。所以这些举动真的只是分散注意力。
3.副本是由于矢量调整大小本身
随着您插入元素,您的std::vector
会逐渐增长 - 需要移动或复制结构。有关详细信息,请参阅@NutCracker的帖子。
4. 真正的问题是例外
看到这个问题:
当向量增长时,如何强制执行移动语义?
std::vector
不知道它可以在调整大小时安全地移动元素 - 其中"安全"意味着"无例外",因此它回退到复制。
5."但是我的复制者也可以抛出异常!
我想基本原理是,如果您在复制较小的缓冲区时遇到异常 - 您仍然没有触及它,因此至少您的原始、未调整大小的矢量是有效的并且可以使用。如果您开始移动元素并得到异常 - 那么无论如何您都没有该元素的有效副本,更不用说有效的较小向量了。
您的移动构造函数没有说明符noexcept
。
声明它像
VecOfInt(VecOfInt&& other) noexcept : m_size(other.m_size) {
std::cout << "move..." << std::endl;
m_data = other.m_data;
other.m_data = nullptr;
}
否则,类模板 std::vector 将调用复制构造函数。
将noexcept
装饰器添加到移动构造函数和移动赋值运算符:
VecOfInt(VecOfInt&& other) noexcept : m_size(other.m_size) {
std::cout << "move..." << std::endl;
m_data = other.m_data;
other.m_data = nullptr;
}
VecOfInt& operator=(VecOfInt&& other) noexcept {
std::cout << "move assignment..." << std::endl;
m_size = other.m_size;
m_data = other.m_data;
other.m_data = nullptr;
return *this;
}
某些函数(例如,std::vector
使用的std::move_if_noexcept
)将决定复制您的对象,如果它的移动操作没有用noexcept
.装饰,即不能保证它们不会抛出。这就是为什么你应该致力于让你的移动操作(移动构造函数,移动赋值运算符)除外。这可以显著提高程序的性能。
根据斯科特·迈耶(Scott Meyer)在《有效的现代C++》中的说法:
std::vector
利用这种"如果可以,则移动,但复制如果 你必须"策略,它不是标准中的唯一功能 图书馆确实如此。其他功能具有强大的异常安全性 保证 C++98 (例如std::vector::reserve
,std::deque::insert
, 等)的行为方式相同。所有这些函数都替换了对复制的调用 C++98 中的操作,仅在以下情况下调用 C++11 中的移动操作 已知移动操作不会发出异常。但是,一个 函数知道移动操作是否不会产生异常?这 答案很明显:它检查操作是否已声明 不,除了。
- 为什么复制而不是移动数据元素?
- 设计将引用元素移动到开头的数据结构.C++
- 如果变量数据包含大于 vector 所有元素的整数,则仅在视觉工作室上接收"矢量下标超出范围"?
- 保持排序的数据结构,允许log N插入时间,并且可以返回我在log N中查找的元素的索引
- 如何为结构的数据元素赋值
- C++ Expat 仅打印元素的第一个字母和标签中的数据
- C++ STL 数据结构常时按索引推送/弹出/随机访问,并具有指向元素的可靠指针
- 从自定义数据类型向量中删除重复元素
- C++ - 按自定义数据类型向量的值删除元素
- Getter 函数,用于在 2d 数组是类的数据成员时检索 2d 数组元素
- unordered_set是否适合存储矢量<int>元素的数据结构?如果是这样,我将如何实现哈希函数?
- 使用 dectlype 推断模板元素类型中的数据类型是否正确?
- 用于查找元素的数据结构
- C++ 数据结构队列:使用 for 循环查找队列中最大的元素
- 尝试读取数据文件,存储在数组中并打印所有元素,但它不起作用
- 派生类中的数据元素
- 如何比较和存储 2 个向量的数据元素位置
- 如何在c++中处理大数据元素
- 链表,每个键包含多个数据元素
- 模板链表获取复杂数据类型的数据元素