为什么保留额外内存时向量中的对象没有移动?
Why is there no movement of objects in the vector when additional memory is reserved?
有如下代码:
#include <iostream>
#include <vector>
class Test {
public:
Test() {}
Test(int x) : x(x) {}
Test(const Test&) noexcept = delete;
Test& operator=(const Test&) noexcept = delete;
Test(Test&& r) noexcept { x = std::move(r.x); }
Test& operator=(Test&& r) noexcept {
std::cout << "move";
x = std::move(r.x);
return *this;
}
~Test() noexcept { std::cout << x; }
private:
int x;
};
int main() {
std::vector<Test> v;
v.reserve(5);
v.emplace_back(1);
v.emplace_back(2);
v.emplace_back(3);
v.emplace_back(4);
v.emplace_back(5);
v.reserve(10);
}
输出:
1234512345
我们看到了析构函数,但没有看到移动赋值,因此我们可以断定对象正在被复制而不是被移动。
这个问题已经在几个话题中讨论过了,但是没有一个具体的答案。
使用 unique_ptrs 有一个解决方法:
#include <iostream>
#include <vector>
#include <memory>
class Test {
public:
Test() {}
Test(int x) : x(x) {}
Test(const Test&) noexcept = delete;
Test& operator=(const Test&) noexcept = delete;
Test(Test&& r) noexcept { x = std::move(r.x); }
Test& operator=(Test&& r) noexcept {
std::cout << "move";
x = std::move(r.x);
return *this;
}
~Test() noexcept { std::cout << x; }
private:
int x;
};
int main() {
std::vector<std::unique_ptr<Test>> v;
v.reserve(5);
v.emplace_back(std::make_unique<Test>(1));
v.emplace_back(std::make_unique<Test>(2));
v.emplace_back(std::make_unique<Test>(3));
v.emplace_back(std::make_unique<Test>(4));
v.emplace_back(std::make_unique<Test>(5));
v.reserve(10);
}
输出:
12345
所有代码在编译器 gcc 5.1 - 10.2 上测试
为什么在 vector 中分配内存而不是移动时会发生对象复制?为什么第一个代码块中没有使用移动语义,这种行为的原因是什么?
移动constructor/operator 应该使旧对象处于未定义但有效 状态。在这种情况下,编译器选择保持不变并简单地复制值。当这些对象组中的每一个都被破坏时(首先是移动之后,然后是 std::vector
析构函数),它们会打印您看到的值。
语句 v.reserve(10);
使您的向量重新分配其存储空间,因此它可以容纳至少 10 个元素。假设 capacity
小于 10,这是一个四步过程:
新存储已分配,足够 capacity
至少为 10。
需要将先前存储中的元素移动到新存储中。对于先前存储中的每个元素,将该元素的值移动到新存储中的新实例中。
在释放旧存储之前,移出的实例仍然需要销毁。这会导致它们的每个析构函数执行,在您的情况下会打印 12345
。请注意,每个移出的实例仍具有其旧值 x
。移动一个int
相当于复制它,原值不变
现在释放了以前的存储空间。矢量现在使用新存储,它包含等效元素但比以前的存储更大。
最后,在 main
结束时,您的向量 v
必须被销毁。这也会导致它的每个元素都被销毁,从而导致每个析构函数都被执行。这是打印的第二组 12345
。
使用 std::unique_ptr
您只能看到一组数字 (12345
),因为 unique_ptr
在两个存储之间移动,而不是 Test
。 std::unique_ptr<Test>
的新移动构造实例拥有指针的所有权,而旧的移动实例不再拥有这些指针。当先前存储中的实例被销毁时,它们的析构函数什么都不做(因为它们不再拥有指针)。当 v
在 main
结束时销毁时,您只会看到 Test
实例的销毁。然后,被销毁的 std::unique_ptr
个实例 do 拥有指向 Test
的指针,因此导致 Test
个实例成为 delete
d。
编辑:请注意,您只检测了移动赋值运算符。在这种情况下,std::vector
将使用移动构造函数,因此即使 Test
的实例被 v
移动,您也不会看到 "move"
。
有如下代码:
#include <iostream>
#include <vector>
class Test {
public:
Test() {}
Test(int x) : x(x) {}
Test(const Test&) noexcept = delete;
Test& operator=(const Test&) noexcept = delete;
Test(Test&& r) noexcept { x = std::move(r.x); }
Test& operator=(Test&& r) noexcept {
std::cout << "move";
x = std::move(r.x);
return *this;
}
~Test() noexcept { std::cout << x; }
private:
int x;
};
int main() {
std::vector<Test> v;
v.reserve(5);
v.emplace_back(1);
v.emplace_back(2);
v.emplace_back(3);
v.emplace_back(4);
v.emplace_back(5);
v.reserve(10);
}
输出:
1234512345
我们看到了析构函数,但没有看到移动赋值,因此我们可以断定对象正在被复制而不是被移动。 这个问题已经在几个话题中讨论过了,但是没有一个具体的答案。
使用 unique_ptrs 有一个解决方法:
#include <iostream>
#include <vector>
#include <memory>
class Test {
public:
Test() {}
Test(int x) : x(x) {}
Test(const Test&) noexcept = delete;
Test& operator=(const Test&) noexcept = delete;
Test(Test&& r) noexcept { x = std::move(r.x); }
Test& operator=(Test&& r) noexcept {
std::cout << "move";
x = std::move(r.x);
return *this;
}
~Test() noexcept { std::cout << x; }
private:
int x;
};
int main() {
std::vector<std::unique_ptr<Test>> v;
v.reserve(5);
v.emplace_back(std::make_unique<Test>(1));
v.emplace_back(std::make_unique<Test>(2));
v.emplace_back(std::make_unique<Test>(3));
v.emplace_back(std::make_unique<Test>(4));
v.emplace_back(std::make_unique<Test>(5));
v.reserve(10);
}
输出:
12345
所有代码在编译器 gcc 5.1 - 10.2 上测试
为什么在 vector 中分配内存而不是移动时会发生对象复制?为什么第一个代码块中没有使用移动语义,这种行为的原因是什么?
移动constructor/operator 应该使旧对象处于未定义但有效 状态。在这种情况下,编译器选择保持不变并简单地复制值。当这些对象组中的每一个都被破坏时(首先是移动之后,然后是 std::vector
析构函数),它们会打印您看到的值。
语句 v.reserve(10);
使您的向量重新分配其存储空间,因此它可以容纳至少 10 个元素。假设 capacity
小于 10,这是一个四步过程:
新存储已分配,足够
capacity
至少为 10。需要将先前存储中的元素移动到新存储中。对于先前存储中的每个元素,将该元素的值移动到新存储中的新实例中。
在释放旧存储之前,移出的实例仍然需要销毁。这会导致它们的每个析构函数执行,在您的情况下会打印
12345
。请注意,每个移出的实例仍具有其旧值x
。移动一个int
相当于复制它,原值不变现在释放了以前的存储空间。矢量现在使用新存储,它包含等效元素但比以前的存储更大。
最后,在 main
结束时,您的向量 v
必须被销毁。这也会导致它的每个元素都被销毁,从而导致每个析构函数都被执行。这是打印的第二组 12345
。
使用 std::unique_ptr
您只能看到一组数字 (12345
),因为 unique_ptr
在两个存储之间移动,而不是 Test
。 std::unique_ptr<Test>
的新移动构造实例拥有指针的所有权,而旧的移动实例不再拥有这些指针。当先前存储中的实例被销毁时,它们的析构函数什么都不做(因为它们不再拥有指针)。当 v
在 main
结束时销毁时,您只会看到 Test
实例的销毁。然后,被销毁的 std::unique_ptr
个实例 do 拥有指向 Test
的指针,因此导致 Test
个实例成为 delete
d。
编辑:请注意,您只检测了移动赋值运算符。在这种情况下,std::vector
将使用移动构造函数,因此即使 Test
的实例被 v
移动,您也不会看到 "move"
。