Effective C++ 条款11:在 operator= 中处理自我赋值
·
Effective C++ 条款11:在 operator= 中处理自我赋值
确保当对象自我赋值时
operator=有良好行为。其中技术包括比较"来源对象"和"目标对象"的地址、精心周到的语句顺序、以及 copy-and-swap。
一、什么是自我赋值?
自我赋值(self-assignment)发生在对象被赋值给自己时。最直观的例子是:
Widget w;
w = w; // 明显的自我赋值
但在实际开发中,自我赋值往往以更隐蔽的形式出现:
Widget w1, w2;
Widget* pw1 = &w1;
Widget* pw2 = &w1; // pw1 和 pw2 指向同一对象
*pw1 = *pw2; // 隐式自我赋值!
// 更隐蔽的情况
Widget& rw1 = w1;
Widget& rw2 = w1;
rw1 = rw2; // 同样是自我赋值
// 数组中的元素
Widget array[10];
array[i] = array[j]; // 当 i == j 时,自我赋值发生
自我赋值并不总是肉眼可见的
x = x,通过指针、引用或别名访问时同样可能发生。
二、自我赋值带来的问题
让我们看一个典型的资源管理类,假设它管理一个位图(Bitmap):
class Bitmap {
public:
Bitmap(size_t width, size_t height);
~Bitmap();
// ...
private:
size_t width_, height_;
int* pixels_;
};
class Widget {
public:
Widget& operator=(const Widget& rhs) {
delete pb_; // 删除当前持有的 Bitmap
pb_ = new Bitmap(*rhs.pb_); // 复制 rhs 的 Bitmap
return *this;
}
private:
Bitmap* pb_;
};
问题分析
当发生自我赋值 w = w 时:
| 步骤 | 操作 | 结果 |
|---|---|---|
| 1 | delete pb_ |
删除了 w 自己的 Bitmap |
| 2 | new Bitmap(*rhs.pb_) |
rhs.pb_ 指向的内存已被释放!悬空指针访问! |
最终结果是:w 持有一个指向已删除对象的指针,程序行为未定义(UB)。
三、解决方案
方案一:证同测试(Identity Test)
最直观的方法是在赋值前检查是否是同一个对象:
Widget& Widget::operator=(const Widget& rhs) {
if (this == &rhs) { // 证同测试
return *this; // 如果是自我赋值,直接返回
}
delete pb_;
pb_ = new Bitmap(*rhs.pb_);
return *this;
}
优点:
- 简单直观,容易理解
- 能正确处理自我赋值
缺点:
- 仅在自我赋值时有效,异常安全性不足
- 如果
new Bitmap抛出异常,pb_将指向已删除的内存
方案二:精心安排语句顺序(保证异常安全)
更好的做法是先复制新资源,再释放旧资源:
Widget& Widget::operator=(const Widget& rhs) {
Bitmap* pOrig = pb_; // 记住原来的 pb_
pb_ = new Bitmap(*rhs.pb_); // 先复制 rhs 的 Bitmap
delete pOrig; // 再删除原来的 Bitmap
return *this;
}
分析:
| 场景 | 行为 |
|---|---|
| 正常赋值 | 先复制新资源,成功后释放旧资源 |
| 自我赋值 | *rhs.pb_ 就是 *pb_,但在 delete 前已完成复制,安全 |
new 异常 |
pb_ 仍指向原来的资源,对象状态保持一致(异常安全) |
这个版本同时解决了自我赋值和异常安全问题,是工业级代码的常用写法。
方案三:copy-and-swap 惯用法(现代 C++ 推荐)
最优雅、最现代的解决方案:
class Widget {
public:
Widget& operator=(const Widget& rhs) {
Widget temp(rhs); // 复制构造临时对象
swap(temp); // 与临时对象交换资源
return *this; // 临时对象析构,自动释放旧资源
}
void swap(Widget& other) noexcept {
using std::swap;
swap(pb_, other.pb_);
}
private:
Bitmap* pb_;
};
// C++11 更简洁的版本
Widget& Widget::operator=(Widget rhs) { // 按值传递,已复制
swap(rhs); // 与副本交换
return *this; // 副本析构释放旧资源
}
copy-and-swap 的优势:
- 异常安全:复制在函数外部完成,若复制失败原对象不受影响
- 自我赋值安全:交换操作天然安全
- 代码复用:复用了拷贝构造函数和析构函数的逻辑
- 强异常安全保证:要么完全成功,要么保持原状态不变
四、三种方案对比
| 方案 | 自我赋值安全 | 异常安全 | 代码复杂度 | 推荐程度 |
|---|---|---|---|---|
| 证同测试 | 是 | 否(基本保证) | 低 | 不推荐单独使用 |
| 精心安排顺序 | 是 | 是(强保证) | 中 | 推荐 |
| copy-and-swap | 是 | 是(强保证) | 低(复用代码) | 强烈推荐 |
五、实际应用场景
场景1:字符串类实现
class MyString {
public:
MyString& operator=(const MyString& rhs) {
// copy-and-swap 惯用法
MyString temp(rhs);
swap(temp);
return *this;
}
MyString& operator=(const char* str) {
MyString temp(str);
swap(temp);
return *this;
}
private:
char* data_;
size_t len_;
void swap(MyString& other) noexcept {
using std::swap;
swap(data_, other.data_);
swap(len_, other.len_);
}
};
场景2:管理文件句柄的类
class FileHandle {
public:
FileHandle(const char* filename)
: fd_(fopen(filename, "r")) {
if (!fd_) throw std::runtime_error("Failed to open file");
}
FileHandle& operator=(const FileHandle& rhs) {
FileHandle temp(rhs); // 复制构造打开新文件
swap(temp); // 交换文件描述符
return *this;
}
~FileHandle() {
if (fd_) fclose(fd_);
}
private:
FILE* fd_;
void swap(FileHandle& other) noexcept {
using std::swap;
swap(fd_, other.fd_);
}
};
场景3:现代 C++ 使用智能指针
在现代 C++ 中,更推荐直接使用智能指针来避免手动管理:
#include <memory>
class ModernWidget {
public:
ModernWidget& operator=(const ModernWidget& rhs) {
// std::shared_ptr 自动处理引用计数
// std::unique_ptr 需要显式处理
pb_ = std::make_unique<Bitmap>(*rhs.pb_);
return *this;
}
ModernWidget& operator=(ModernWidget&& rhs) noexcept = default;
private:
std::unique_ptr<Bitmap> pb_;
};
六、更多注意事项
1. 确保任何函数操作多个对象时,即使它们是同一对象也能正确行为
// 不仅是 operator=,其他函数也要考虑
void Widget::process(const Widget& other) {
// 如果 other 就是 *this,下面的代码安全吗?
delete pb_;
pb_ = new Bitmap(*other.pb_); // 危险!
}
2. 赋值运算符的返回值
始终返回 *this 的引用,以支持链式赋值:
x = y = z = 42; // 链式赋值需要 operator= 返回引用
3. 现代 C++ 的移动赋值
C++11 起还应实现移动赋值运算符:
Widget& Widget::operator=(Widget&& rhs) noexcept {
if (this != &rhs) { // 移动时证同测试仍有必要
delete pb_;
pb_ = rhs.pb_;
rhs.pb_ = nullptr;
}
return *this;
}
七、总结
请记住:
- 确保当对象自我赋值时
operator=有良好行为- 技术包括:证同测试、精心周到的语句顺序、以及 copy-and-swap
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一对象时,其行为仍然正确
自我赋值看似罕见,但在通过指针/引用操作的代码中屡见不鲜。处理自我赋值不仅是正确性问题,更是异常安全性的重要一环。copy-and-swap 惯用法是现代 C++ 中处理赋值操作的最佳实践,它简洁、安全、可维护,值得在每个自定义赋值运算符中优先考虑。
参考阅读:
- 《Effective C++》第3版,Scott Meyers
- 《C++ Primer》关于异常安全和资源管理的章节
- C++ Core Guidelines: ES.65, C.86
更多推荐


所有评论(0)