为什么我的数据元素被复制而不是被移动?
Why are my data elements being copied instead of moved?
我正在执行一些关于移动语义的测试,我的 class 行为对我来说似乎很奇怪。
给定 mock class 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
为其元素分配一块连续的内存。当分配的内存太短无法存储新元素时,分配一个新块并将所有当前元素从旧块复制到新块。
您可以使用 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 {...}
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;
};
现场live example输出:
move...
move...
move...
move...
move...
move...
这样做是为了保持异常安全。当调整 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;
}
否则 class 模板 std::vector 将调用复制构造函数。
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;
}
};
你仍然得到相同的行为:try it。
2。您确实看到的移动会分散注意力
现在,push_back()
将首先构造一个元素 - foo
在本例中;然后确保向量中有 space;然后 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
try it。所以这些动作真的只是分散注意力。
3。副本是由于矢量本身调整大小
您的 std::vector
随着您插入元素而逐渐增长 - 需要移动或复制构造。有关详细信息,请参阅 。
4。真正的问题是例外
看到这个问题:
How to enforce move semantics when a vector grows?
std::vector
不知道它可以在调整大小时安全地移动元素 - 其中 "safely" 表示 "without exceptions",因此它退回到复制。
5。 "But my copy ctor can throw an exception too!"
我想这是因为如果您在复制较小的缓冲区时遇到异常 - 您仍然没有触及它,那么至少您的原始未调整大小的向量是有效的并且可以使用。如果您开始 移动 元素并遇到异常 - 那么您无论如何都没有该元素的有效副本,更不用说有效的较小向量了。
将 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::move_if_noexcept
,被 std::vector
使用)将决定 复制 你的对象,如果它的移动操作没有用 noexcept
.,即不能保证它们不会抛出。这就是为什么您的目标应该是使您的移动操作(移动构造函数、移动赋值运算符)不例外。这可以显着提高程序的性能。
根据 Scott Meyer 在 Effective Modern C++ 中的说法:
std::vector
takes advantage of this "move if you can, but copy if
you must" strategy , and it's not the only function in the Standard
Library that does. Other functions sporting strong exception safety
guarantee in C++98 (e.g. std::vector::reserve
, std::deque::insert
,
etc) behave the same way. All these functions replace calls to copy
operations in c++98 with calls to move operations in C++11 only if the
move operations are known not to emit exceptions. But how can a
function know if a move operation won't produce an exception? The
answer is obvious: it checks to see if the operation is declared
noexcept.
我正在执行一些关于移动语义的测试,我的 class 行为对我来说似乎很奇怪。
给定 mock class 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
为其元素分配一块连续的内存。当分配的内存太短无法存储新元素时,分配一个新块并将所有当前元素从旧块复制到新块。
您可以使用 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 {...}
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;
};
现场live example输出:
move...
move...
move...
move...
move...
move...
这样做是为了保持异常安全。当调整 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;
}
否则 class 模板 std::vector 将调用复制构造函数。
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;
}
};
你仍然得到相同的行为:try it。
2。您确实看到的移动会分散注意力
现在,push_back()
将首先构造一个元素 - foo
在本例中;然后确保向量中有 space;然后 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
try it。所以这些动作真的只是分散注意力。
3。副本是由于矢量本身调整大小
您的 std::vector
随着您插入元素而逐渐增长 - 需要移动或复制构造。有关详细信息,请参阅
4。真正的问题是例外
看到这个问题:
How to enforce move semantics when a vector grows?
std::vector
不知道它可以在调整大小时安全地移动元素 - 其中 "safely" 表示 "without exceptions",因此它退回到复制。
5。 "But my copy ctor can throw an exception too!"
我想这是因为如果您在复制较小的缓冲区时遇到异常 - 您仍然没有触及它,那么至少您的原始未调整大小的向量是有效的并且可以使用。如果您开始 移动 元素并遇到异常 - 那么您无论如何都没有该元素的有效副本,更不用说有效的较小向量了。
将 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::move_if_noexcept
,被 std::vector
使用)将决定 复制 你的对象,如果它的移动操作没有用 noexcept
.,即不能保证它们不会抛出。这就是为什么您的目标应该是使您的移动操作(移动构造函数、移动赋值运算符)不例外。这可以显着提高程序的性能。
根据 Scott Meyer 在 Effective Modern C++ 中的说法:
std::vector
takes advantage of this "move if you can, but copy if you must" strategy , and it's not the only function in the Standard Library that does. Other functions sporting strong exception safety guarantee in C++98 (e.g.std::vector::reserve
,std::deque::insert
, etc) behave the same way. All these functions replace calls to copy operations in c++98 with calls to move operations in C++11 only if the move operations are known not to emit exceptions. But how can a function know if a move operation won't produce an exception? The answer is obvious: it checks to see if the operation is declared noexcept.