C++移动构造与移动赋值详解与解决方案
移动语义是C++11引入的重要特性,旨在消除不必要的拷贝,提升性能。但错误使用会导致资源泄漏、悬空指针等问题。
1. 移动语义基本概念
1.1 左值、右值与移动语义
#include <utility>
class Resource {
private:
int* data;
size_t size;
public:
Resource(size_t s) : size(s), data(new int[s]) {}
// 拷贝构造函数(深拷贝)
Resource(const Resource& other) : size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + size, data);
}
// 移动构造函数(资源转移)
Resource(Resource&& other) noexcept
: data(other.data), size(other.size) { // 接管资源
other.data = nullptr; // 置空源对象
other.size = 0;
}
};
void value_categories() {
Resource a(100); // a是左值(有名称,有地址)
Resource b = a; // 拷贝构造:a是左值
Resource c = Resource(200); // 移动构造:Resource(200)是右值(临时对象)
Resource d = std::move(a); // 移动构造:std::move将左值a转为右值引用
}
1.2 移动语义的优势
#include <vector>
#include <chrono>
class HeavyResource {
private:
std::vector<int> large_data;
public:
HeavyResource(size_t size) : large_data(size) {
for (size_t i = 0; i < size; ++i) {
large_data[i] = i;
}
}
// 拷贝构造函数
HeavyResource(const HeavyResource& other) : large_data(other.large_data) {
std::cout << "Copy constructor called" << std::endl;
}
// 移动构造函数
HeavyResource(HeavyResource&& other) noexcept
: large_data(std::move(other.large_data)) {
std::cout << "Move constructor called" << std::endl;
}
};
void performance_demo() {
std::vector<HeavyResource> container;
// 没有移动语义:拷贝整个vector
HeavyResource resource(1000000);
container.push_back(resource); // 拷贝构造
// 有移动语义:转移资源
container.push_back(HeavyResource(1000000)); // 移动构造
container.push_back(std::move(resource)); // 移动构造
}
2. 常见问题与陷阱
2.1 移动后使用源对象
// ❌ 错误:移动后使用源对象
class ProblematicMove {
private:
std::string data;
public:
ProblematicMove(std::string str) : data(std::move(str)) {}
ProblematicMove(ProblematicMove&& other) noexcept
: data(std::move(other.data)) {
// other.data现在处于有效但未指定状态
}
void use_after_move() {
ProblematicMove obj("Hello");
ProblematicMove stolen = std::move(obj);
std::cout << obj.data << std::endl; // 未定义行为!
// obj.data可能为空,也可能是"Hello",具体取决于std::string的实现
}
};
2.2 没有标记为noexcept
// ❌ 错误:移动操作可能抛出异常
class ThrowingMove {
private:
std::vector<int> data;
std::unique_ptr<int[]> buffer;
public:
ThrowingMove(std::vector<int> d, size_t buf_size)
: data(std::move(d)), buffer(std::make_unique<int[]>(buf_size)) {}
// 移动构造函数没有noexcept
ThrowingMove(ThrowingMove&& other) {
data = std::move(other.data); // 不会抛出
buffer = std::move(other.buffer); // 不会抛出
// 但编译器不知道,所以不会标记为noexcept
}
};
void noexcept_importance() {
std::vector<ThrowingMove> container;
ThrowingMove obj({1,2,3}, 100);
// 如果移动构造函数不是noexcept,vector可能选择拷贝而不是移动
container.push_back(std::move(obj));
}
2.3 资源泄漏
// ❌ 错误:移动赋值运算符资源泄漏
class LeakingMove {
private:
int* data;
size_t size;
public:
LeakingMove(size_t s) : size(s), data(new int[s]) {}
// 移动构造函数正确
LeakingMove(LeakingMove&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
// 移动赋值运算符错误:没有释放现有资源
LeakingMove& operator=(LeakingMove&& other) noexcept {
// 忘记:delete[] data;
data = other.data; // 资源泄漏!
size = other.size;
other.data = nullptr;
other.size = 0;
return *this;
}
~LeakingMove() {
delete[] data;
}
};
2.4 自移动赋值
// ❌ 错误:没有处理自移动赋值
class SelfMove {
private:
std::unique_ptr<int[]> data;
size_t size;
public:
SelfMove(size_t s) : size(s), data(std::make_unique<int[]>(s)) {}
SelfMove& operator=(SelfMove&& other) noexcept {
// 没有自移动赋值检查
data = std::move(other.data); // 如果是自移动赋值,这里会移动自己
size = other.size;
return *this;
}
};
void self_move_demo() {
SelfMove obj(100);
obj = std::move(obj); // 自移动赋值:obj.data现在为空!
}
3. 正确实现模式
3.1 基本的移动操作实现
// ✅ 正确的移动操作实现
class CorrectMove {
private:
std::unique_ptr<int[]> data;
size_t size;
public:
CorrectMove(size_t s = 0) : size(s), data(size > 0 ? std::make_unique<int[]>(size) : nullptr) {}
// 拷贝构造函数
CorrectMove(const CorrectMove& other) : size(other.size) {
if (size > 0) {
data = std::make_unique<int[]>(size);
std::copy(other.data.get(), other.data.get() + size, data.get());
}
}
// 移动构造函数
CorrectMove(CorrectMove&& other) noexcept
: data(std::move(other.data)), size(other.size) {
other.size = 0;
}
// 拷贝赋值运算符
CorrectMove& operator=(const CorrectMove& other) {
if (this != &other) {
if (size != other.size) {
data = std::make_unique<int[]>(other.size);
size = other.size;
}
if (size > 0) {
std::copy(other.data.get(), other.data.get() + size, data.get());
}
}
return *this;
}
// 移动赋值运算符
CorrectMove& operator=(CorrectMove&& other) noexcept {
if (this != &other) { // 自移动赋值检查
data = std::move(other.data);
size = other.size;
other.size = 0;
}
return *this;
}
~CorrectMove() = default; // unique_ptr自动处理资源释放
};
3.2 拷贝并交换惯用法的移动支持
// ✅ 使用拷贝并交换支持移动语义
class CopyAndSwapWithMove {
private:
std::vector<int> data;
std::string name;
friend void swap(CopyAndSwapWithMove& first, CopyAndSwapWithMove& second) noexcept {
using std::swap;
swap(first.data, second.data);
swap(first.name, second.name);
}
public:
CopyAndSwapWithMove(std::string n, std::vector<int> d)
: name(std::move(n)), data(std::move(d)) {}
// 拷贝构造函数
CopyAndSwapWithMove(const CopyAndSwapWithMove&) = default;
// 移动构造函数
CopyAndSwapWithMove(CopyAndSwapWithMove&& other) noexcept
: CopyAndSwapWithMove("", {}) { // 委托构造创建空对象
swap(*this, other); // 然后交换
}
// 统一的赋值运算符(处理拷贝和移动)
CopyAndSwapWithMove& operator=(CopyAndSwapWithMove other) noexcept {
swap(*this, other);
return *this;
}
// 注意:不需要单独定义移动赋值运算符,上面的版本已经处理了
~CopyAndSwapWithMove() = default;
};
3.3 基于RAII的自动移动语义
// ✅ 使用RAII类型自动获得移动语义
class RAIIBasedMove {
private:
std::vector<std::string> strings; // 自动支持移动
std::unique_ptr<int[]> data; // 自动支持移动
std::shared_ptr<Config> config; // 自动支持移动(共享)
public:
RAIIBasedMove(size_t data_size, std::vector<std::string> strs,
std::shared_ptr<Config> cfg)
: strings(std::move(strs)),
data(data_size > 0 ? std::make_unique<int[]>(data_size) : nullptr),
config(std::move(cfg)) {}
// 不需要自定义移动操作!
// 编译器生成的版本会正确移动所有成员
// 显式默认以明确意图
RAIIBasedMove(const RAIIBasedMove&) = default;
RAIIBasedMove(RAIIBasedMove&&) noexcept = default;
RAIIBasedMove& operator=(const RAIIBasedMove&) = default;
RAIIBasedMove& operator=(RAIIBasedMove&&) noexcept = default;
~RAIIBasedMove() = default;
// 提供深拷贝接口(如果需要)
RAIIBasedMove deepCopy() const {
RAIIBasedMove copy = *this; // 浅拷贝
// 对需要深拷贝的成员进行特殊处理
if (data) {
size_t size = /* 需要记录大小 */;
copy.data = std::make_unique<int[]>(size);
std::copy(data.get(), data.get() + size, copy.data.get());
}
return copy;
}
};
4. 高级主题与最佳实践
4.1 条件性noexcept
#include <type_traits>
class ConditionalNoexcept {
private:
std::vector<int> data;
std::string name;
public:
ConditionalNoexcept(std::string n, std::vector<int> d)
: name(std::move(n)), data(std::move(d)) {}
// 移动构造函数:根据成员移动操作是否noexcept
ConditionalNoexcept(ConditionalNoexcept&& other)
noexcept(std::is_nothrow_move_constructible_v<std::vector<int>> &&
std::is_nothrow_move_constructible_v<std::string>)
: data(std::move(other.data)), name(std::move(other.name)) {}
// 移动赋值运算符:条件性noexcept
ConditionalNoexcept& operator=(ConditionalNoexcept&& other)
noexcept(std::is_nothrow_move_assignable_v<std::vector<int>> &&
std::is_nothrow_move_assignable_v<std::string>) {
if (this != &other) {
data = std::move(other.data);
name = std::move(other.name);
}
return *this;
}
};
4.2 移动感知的成员函数
class MoveAware {
private:
std::vector<int> data;
public:
MoveAware(std::vector<int> d) : data(std::move(d)) {}
// 针对左值的版本
void addData(const std::vector<int>& new_data) & {
std::cout << "Adding data by copy\n";
data.insert(data.end(), new_data.begin(), new_data.end());
}
// 针对右值的版本
void addData(std::vector<int>&& new_data) & {
std::cout << "Adding data by move\n";
// 直接移动,避免拷贝
if (data.empty()) {
data = std::move(new_data);
} else {
data.insert(data.end(),
std::make_move_iterator(new_data.begin()),
std::make_move_iterator(new_data.end()));
}
}
// 在右值对象上调用时的版本
void addData(const std::vector<int>& new_data) && {
std::cout << "Adding data to temporary object\n";
// 因为是临时对象,可以直接修改
data.insert(data.end(), new_data.begin(), new_data.end());
}
};
void use_move_aware() {
MoveAware obj({1, 2, 3});
std::vector<int> new_data{4, 5, 6};
obj.addData(new_data); // 调用拷贝版本
obj.addData(std::move(new_data)); // 调用移动版本
MoveAware({1,2,3}).addData({4,5,6}); // 调用右值版本
}
4.3 完美转发在移动语义中的应用
class PerfectForwarding {
private:
std::vector<std::string> items;
public:
// 完美转发构造函数
template<typename T, typename... Args>
void emplaceBack(T&& first, Args&&... args) {
// 完美转发所有参数
items.emplace_back(std::forward<T>(first), std::forward<Args>(args)...);
}
// 设置数据的完美转发版本
template<typename T>
void setData(T&& new_data) {
// 根据传入的是左值还是右值,选择拷贝或移动
items = std::forward<T>(new_data);
}
};
void use_perfect_forwarding() {
PerfectForwarding pf;
std::vector<std::string> data{"hello", "world"};
pf.emplaceBack("test"); // 转发const char*
pf.setData(data); // 拷贝
pf.setData(std::move(data)); // 移动
std::string temp = "temporary";
pf.emplaceBack(std::move(temp)); // 移动
}
5. 特定场景解决方案
5.1 包含不可移动成员的处理
class WithImmobileMember {
private:
std::atomic<int> counter; // 不可移动
std::vector<int> data; // 可移动
public:
WithImmobileMember(int init_count, std::vector<int> d)
: counter(init_count), data(std::move(d)) {}
// 移动构造函数:需要特殊处理不可移动成员
WithImmobileMember(WithImmobileMember&& other) noexcept
: counter(other.counter.load()), // 原子变量需要显式拷贝
data(std::move(other.data)) { // 其他成员可以移动
}
// 移动赋值运算符
WithImmobileMember& operator=(WithImmobileMember&& other) noexcept {
if (this != &other) {
counter.store(other.counter.load()); // 原子操作
data = std::move(other.data);
}
return *this;
}
// 注意:原子类型不可移动,但可以拷贝其值
};
5.2 多态基类的移动语义
class CloneableBase {
public:
virtual ~CloneableBase() = default;
// 禁止拷贝但允许移动
CloneableBase(const CloneableBase&) = delete;
CloneableBase& operator=(const CloneableBase&) = delete;
// 默认移动操作
CloneableBase(CloneableBase&&) = default;
CloneableBase& operator=(CloneableBase&&) = default;
// 克隆接口
virtual std::unique_ptr<CloneableBase> clone() const = 0;
// 移动克隆接口
virtual std::unique_ptr<CloneableBase> move_clone() && = 0;
};
class Derived : public CloneableBase {
private:
std::vector<int> data;
std::string name;
public:
Derived(std::string n, std::vector<int> d)
: name(std::move(n)), data(std::move(d)) {}
std::unique_ptr<CloneableBase> clone() const override {
return std::make_unique<Derived>(*this);
}
std::unique_ptr<CloneableBase> move_clone() && override {
return std::make_unique<Derived>(std::move(*this));
}
// 派生类需要可移动
Derived(Derived&&) = default;
Derived& operator=(Derived&&) = default;
// 注意:基类已删除拷贝操作,派生类也不能拷贝
};
5.3 移动语义与异常安全
class ExceptionSafeMove {
private:
std::unique_ptr<int[]> part1;
std::unique_ptr<double[]> part2;
std::string name;
public:
ExceptionSafeMove(size_t s1, size_t s2, std::string n)
: part1(s1 > 0 ? std::make_unique<int[]>(s1) : nullptr),
part2(s2 > 0 ? std::make_unique<double[]>(s2) : nullptr),
name(std::move(n)) {}
// 强异常安全的移动赋值
ExceptionSafeMove& operator=(ExceptionSafeMove&& other) noexcept {
if (this != &other) {
// 先创建临时对象接管新资源
auto new_part1 = std::move(other.part1);
auto new_part2 = std::move(other.part2);
auto new_name = std::move(other.name);
// 然后交换(不会抛出异常)
part1 = std::move(new_part1);
part2 = std::move(new_part2);
name = std::move(new_name);
}
return *this;
}
// 或者使用交换惯用法
friend void swap(ExceptionSafeMove& first, ExceptionSafeMove& second) noexcept {
using std::swap;
swap(first.part1, second.part1);
swap(first.part2, second.part2);
swap(first.name, second.name);
}
ExceptionSafeMove& operator=(ExceptionSafeMove other) noexcept {
swap(*this, other);
return *this;
}
};
6. 测试与验证
6.1 移动语义正确性测试
#include <cassert>
void test_move_semantics() {
// 测试移动构造函数
{
CorrectMove original(100);
int* original_data = original.getData(); // 假设有getData()方法
CorrectMove moved = std::move(original);
assert(moved.getData() == original_data); // 资源被转移
assert(original.getData() == nullptr); // 源对象被置空
assert(original.getSize() == 0);
}
// 测试移动赋值运算符
{
CorrectMove a(50);
CorrectMove b(100);
int* a_data = a.getData();
b = std::move(a);
assert(b.getData() == a_data); // 资源转移
assert(a.getData() == nullptr); // 源对象置空
}
// 测试自移动赋值
{
CorrectMove obj(75);
int* original_data = obj.getData();
obj = std::move(obj); // 应该没有影响
assert(obj.getData() == original_data); // 资源保持不变
assert(obj.getSize() == 75);
}
// 测试异常安全
{
ExceptionSafeMove obj1(10, 20, "test1");
ExceptionSafeMove obj2(30, 40, "test2");
// 移动赋值应该是异常安全的
obj1 = std::move(obj2);
assert(obj1.getName() == "test2");
assert(obj2.getName().empty()); // 被移动后的有效状态
}
std::cout << "All move semantics tests passed!" << std::endl;
}
6.2 性能测试
#include <chrono>
class PerformanceTest {
private:
std::vector<int> data;
public:
PerformanceTest(size_t size) : data(size) {
std::iota(data.begin(), data.end(), 0);
}
// 拷贝构造函数
PerformanceTest(const PerformanceTest& other) : data(other.data) {}
// 移动构造函数
PerformanceTest(PerformanceTest&& other) noexcept : data(std::move(other.data)) {}
};
void benchmark_move_vs_copy() {
const int iterations = 10000;
const size_t data_size = 10000;
// 测试拷贝性能
auto start = std::chrono::high_resolution_clock::now();
std::vector<PerformanceTest> copy_container;
copy_container.reserve(iterations);
PerformanceTest prototype(data_size);
for (int i = 0; i < iterations; ++i) {
copy_container.push_back(prototype); // 拷贝
}
auto copy_time = std::chrono::high_resolution_clock::now() - start;
// 测试移动性能
start = std::chrono::high_resolution_clock::now();
std::vector<PerformanceTest> move_container;
move_container.reserve(iterations);
for (int i = 0; i < iterations; ++i) {
PerformanceTest temp(data_size);
move_container.push_back(std::move(temp)); // 移动
}
auto move_time = std::chrono::high_resolution_clock::now() - start;
std::cout << "Copy time: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(copy_time).count()
<< " ms" << std::endl;
std::cout << "Move time: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(move_time).count()
<< " ms" << std::endl;
}
7. 最佳实践总结
7.1 移动语义设计原则
// 移动语义最佳实践模板
class MoveBestPractices {
private:
std::vector<int> data;
std::string name;
std::unique_ptr<Resource> resource;
public:
MoveBestPractices(std::string n, std::vector<int> d, std::unique_ptr<Resource> res)
: name(std::move(n)), data(std::move(d)), resource(std::move(res)) {}
// 1. 移动操作标记为noexcept
MoveBestPractices(MoveBestPractices&& other) noexcept
: data(std::move(other.data)),
name(std::move(other.name)),
resource(std::move(other.resource)) {}
// 2. 移动赋值运算符处理自赋值
MoveBestPractices& operator=(MoveBestPractices&& other) noexcept {
if (this != &other) {
data = std::move(other.data);
name = std::move(other.name);
resource = std::move(other.resource);
}
return *this;
}
// 3. 移动后置源对象于有效状态
// 对于标准库类型,移动后处于"有效但未指定状态"
// 对于自定义类型,通常置为默认构造状态
// 4. 优先使用编译器生成的版本(规则零)
// 5. 需要时使用拷贝并交换惯用法
~MoveBestPractices() = default;
};
7.2 使用场景指南
| 场景 | 推荐做法 | 注意事项 |
|---|---|---|
| 管理动态资源 | 实现移动操作,转移资源所有权 | 确保noexcept,处理自赋值 |
| 包含标准库成员 | 使用编译器生成的移动操作 | 大多数情况不需要自定义 |
| 性能敏感代码 | 提供移动感知的重载 | 使用引用限定符 |
| 多态基类 | 默认移动操作,或禁止拷贝允许移动 | 注意切片问题 |
| 异常安全要求 | 使用交换惯用法 | 确保交换操作noexcept |
7.3 常见陷阱总结
// ❌ 避免这些陷阱
class MovePitfalls {
public:
// 陷阱1:移动后使用源对象
void pitfall1() {
std::string str = "hello";
std::string stolen = std::move(str);
// cout << str; // 错误:str处于有效但未指定状态
}
// 陷阱2:移动操作不是noexcept
// 这会影响标准库容器的效率
// 陷阱3:忘记释放现有资源(移动赋值)
// 陷阱4:没有处理自移动赋值
// 陷阱5:在返回值中使用std::move(抑制RVO)
std::vector<int> createVector() {
std::vector<int> result{1, 2, 3};
return result; // 正确:可能RVO或移动
// return std::move(result); // 错误:抑制RVO
}
};
通过遵循这些最佳实践,可以安全高效地使用移动语义,显著提升C++程序的性能,同时避免常见的陷阱和错误。
1万+

被折叠的 条评论
为什么被折叠?



