23. 如何禁用拷贝但允许移动?如何禁用移动但允许拷贝?
这个问题,说白了就是问:你怎么控制一个对象的“复制”和“搬家”权限?
我在项目里见过不少新手,上来就把拷贝构造函数和移动构造函数全删了,结果发现标准库容器根本用不了。嗯,这里要讲清楚:拷贝和移动是两回事,控制权在你手里。
一、禁用拷贝,但允许移动
这种场景很常见。比如你有一个管理动态内存的类,拷贝起来太贵了,你希望用户只能用移动来转移所有权。典型的例子就是 std::unique_ptr。
怎么做?把拷贝构造函数和拷贝赋值运算符标记为 = delete,然后自己实现移动构造函数和移动赋值运算符。
class Buffer {
public:
// 构造函数
Buffer(size_t size) : size_(size), data_(new char[size]) {}
// 禁用拷贝
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
// 允许移动
Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
}
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = other.data_;
other.size_ = 0;
other.data_ = nullptr;
}
return *this;
}
~Buffer() { delete[] data_; }
private:
size_t size_;
char* data_;
};
这样写之后,下面的代码就能正常编译:
Buffer a(1024);
Buffer b = std::move(a); // OK,移动构造
Buffer c(std::move(b)); // OK,移动构造
但如果你试图拷贝:
Buffer d = a; // 编译错误!拷贝构造被删除
注意: 我曾经在代码审查时发现有人忘了给移动操作加 noexcept。结果 std::vector 在扩容时,宁愿选择拷贝也不调用移动构造。为什么?因为标准库需要保证强异常安全。如果你的移动操作可能抛异常,它就不敢用。所以,移动操作一定要加 noexcept。
二、禁用移动,但允许拷贝
这个场景相对少见,但也不是没有。比如你有一个对象,它的身份标识很重要,你不想让用户“偷走”它的内部状态。或者,你的对象内部有指向自身的指针,移动后指针就失效了。
做法正好相反:把移动构造函数和移动赋值运算符标记为 = delete,保留拷贝操作。
class ImmutableBuffer {
public:
ImmutableBuffer(size_t size) : size_(size), data_(new char[size]) {}
// 允许拷贝(深拷贝)
ImmutableBuffer(const ImmutableBuffer& other)
: size_(other.size_), data_(new char[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
}
ImmutableBuffer& operator=(const ImmutableBuffer& other) {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = new char[size_];
std::copy(other.data_, other.data_ + size_, data_);
}
return *this;
}
// 禁用移动
ImmutableBuffer(ImmutableBuffer&&) = delete;
ImmutableBuffer& operator=(ImmutableBuffer&&) = delete;
~ImmutableBuffer() { delete[] data_; }
private:
size_t size_;
char* data_;
};
这样,用户只能拷贝,不能移动:
ImmutableBuffer a(1024);
ImmutableBuffer b = a; // OK,拷贝构造
ImmutableBuffer c = std::move(a); // 编译错误!移动构造被删除
一个小技巧: 如果你想让一个类既不能拷贝也不能移动,那就把拷贝和移动都 = delete。但说实话,这种类很少见,除非你是在写一个单例或者某个全局资源管理器。
三、核心逻辑图
下面这张图帮你理清思路:
四、一些实战经验
我个人习惯是:如果一个类管理了资源(内存、文件句柄、网络连接等),我默认会禁用拷贝,只允许移动。为什么?因为深拷贝太容易出性能问题了。我在项目中遇到过好几次,因为不小心拷贝了一个大对象,导致内存暴涨,程序直接 OOM。
反过来,如果一个类不管理资源,只是简单的数据聚合(比如一个 Point 结构体),那我连移动都不会去写,直接用默认的拷贝就行。移动对于小对象来说,其实没什么性能优势。
避坑指南: 我曾经在写一个网络库时,把 Socket 类的移动构造函数写成了深拷贝。结果客户端连接一多,内存就炸了。后来才发现,移动构造里忘了把源对象的文件描述符置为 -1,导致析构时把资源给释放了。所以,移动操作一定要记得“清空源对象”,否则就是假移动。
五、总结一下
| 策略 | 拷贝构造/赋值 | 移动构造/赋值 | 典型场景 |
|---|---|---|---|
| 禁用拷贝,允许移动 | = delete |
自定义实现 + noexcept |
管理动态内存、文件句柄、unique_ptr |
| 禁用移动,允许拷贝 | 自定义实现 | = delete |
对象有内部自引用、身份标识重要 |
| 两者都禁用 | = delete |
= delete |
单例、全局资源管理器 |
说白了,拷贝和移动的控制,就是你对对象生命周期的管理策略。想清楚你的类到底需不需要“复制”,需不需要“搬家”,然后果断地删掉或实现对应的函数。别犹豫,犹豫就会败北。