Effective C++ 条款10:令 operator= 返回一个 reference to *this
在 C++ 中,赋值运算符的重载是一个看似简单却暗藏玄机的主题。你有没有想过,为什么
x = y = z = 15这样的连锁赋值能够正常工作?答案就藏在operator=的返回值中。
一、连锁赋值的魅力
在 C++ 中,我们可以写出这样的代码:
cpp
int x, y, z;
x = y = z = 15;
这段代码的执行顺序是(从右到左):
z = 15 -> 返回 z 的引用
y = (z=15) -> 返回 y 的引用
x = (y=...) -> 返回 x 的引用
最终,x、y、z 的值都是 15。
这个特性不仅适用于内置类型,也应该适用于我们自定义的类。但如果我们不特别注意 operator= 的实现,这种优雅的语法就无法使用。
二、错误的实现方式
让我们看看如果不遵循这个条款会发生什么:
2.1 返回 void
cpp
class Widget {
public:
void operator=(const Widget& rhs) { // 返回 void
data_ = rhs.data_;
}
private:
int data_;
};
Widget w1, w2, w3;
w1 = w2 = w3; // 编译错误!void 不能作为赋值操作的右操作数
编译器会报错,因为 w2 = w3 返回 void,而 w1 = void 是非法的。
2.2 返回对象(值返回)
cpp
class Widget {
public:
Widget operator=(const Widget& rhs) { // 返回对象(拷贝)
data_ = rhs.data_;
return *this; // 返回的是 *this 的拷贝
}
private:
int data_;
};
虽然这样支持连锁赋值,但有一个严重问题:效率低下。
cpp
w1 = w2 = w3;
执行过程:
w2 = w3返回Widget的临时拷贝w1 = (临时拷贝)再次赋值- 临时拷贝被销毁
如果 Widget 是一个大型对象,这种不必要的拷贝开销是巨大的。
2.3 返回 const 引用
cpp
class Widget {
public:
const Widget& operator=(const Widget& rhs) { // 返回 const 引用
data_ = rhs.data_;
return *this;
}
private:
int data_;
};
这样可以工作,但限制了灵活性:
cpp
(w1 = w2) = w3; // 编译错误!const 引用不能作为左值
虽然 (w1 = w2) = w3 这种写法不常见,但内置类型是支持这种语法的。为了保持与内置类型行为的一致性,我们应该返回非 const 引用。
三、正确的实现方式
3.1 标准写法
cpp
class Widget {
public:
Widget& operator=(const Widget& rhs) { // 返回非 const 引用
data_ = rhs.data_;
return *this; // 返回左操作数的引用
}
private:
int data_;
};
现在:
cpp
Widget w1, w2, w3;
w1 = w2 = w3; // OK
(w1 = w2) = w3; // OK,虽然不太常见
3.2 为什么返回 Widget& 而不是 Widget*?
| 返回类型 | 语法 | 与内置类型一致性 |
|---|---|---|
Widget& |
w1 = w2 = w3 |
完全一致 |
Widget* |
*(w1 = w2) = w3 |
不一致,需要解引用 |
返回引用让自定义类型的赋值操作与内置类型的行为完全一致,这是 C++ 运算符重载的重要原则。
四、连锁赋值的原理
4.1 赋值运算符的右结合性
C++ 中,赋值运算符是右结合的:
cpp
x = y = z = 15;
等价于:
cpp
x = (y = (z = 15));
4.2 执行流程图解
步骤 1: z = 15
+------------------+
| z.operator=(15) |
| z.data_ = 15 |
| return z& |----+
+------------------+ |
v
步骤 2: y = (z=15) +
+------------------+ |
| y.operator=(z) |<---+
| y.data_ = 15 |
| return y& |----+
+------------------+ |
v
步骤 3: x = (y=...) +
+------------------+ |
| x.operator=(y) |<---+
| x.data_ = 15 |
| return x& |
+------------------+
正是因为每个 operator= 都返回了左操作数的引用,连锁赋值才能像链条一样一环扣一环地执行。
五、其他赋值相关运算符
这个规则不仅适用于 operator=,也适用于所有赋值相关的运算符:
cpp
class Widget {
public:
// 赋值运算符
Widget& operator=(const Widget& rhs) {
// ...
return *this;
}
// 移动赋值运算符 (C++11)
Widget& operator=(Widget&& rhs) noexcept {
// ...
return *this;
}
// 复合赋值运算符
Widget& operator+=(const Widget& rhs) {
data_ += rhs.data_;
return *this;
}
Widget& operator-=(const Widget& rhs) {
data_ -= rhs.data_;
return *this;
}
Widget& operator*=(const Widget& rhs) {
data_ *= rhs.data_;
return *this;
}
Widget& operator/=(const Widget& rhs) {
data_ /= rhs.data_;
return *this;
}
// 位运算复合赋值
Widget& operator&=(const Widget& rhs) {
data_ &= rhs.data_;
return *this;
}
Widget& operator|=(const Widget& rhs) {
data_ |= rhs.data_;
return *this;
}
private:
int data_;
};
5.1 与 STL 的一致性
STL 容器也遵循这个约定:
cpp
std::vector<int> v1, v2, v3;
v1 = v2 = v3; // 完全支持
std::string s1, s2, s3;
s1 = s2 = s3; // 完全支持
查看 std::vector 的源码(简化版):
cpp
template<typename T>
class vector {
public:
vector& operator=(const vector& rhs) {
// ... 赋值逻辑 ...
return *this;
}
vector& operator=(vector&& rhs) noexcept {
// ... 移动赋值逻辑 ...
return *this;
}
};
六、实际应用场景
6.1 矩阵运算库
cpp
class Matrix {
public:
Matrix(int rows, int cols) : rows_(rows), cols_(cols) {
data_ = new double[rows * cols]();
}
// 拷贝赋值
Matrix& operator=(const Matrix& rhs) {
if (this == &rhs) return *this; // 自赋值检查
// 重新分配内存(如果需要)
if (rows_ != rhs.rows_ || cols_ != rhs.cols_) {
delete[] data_;
data_ = new double[rhs.rows_ * rhs.cols_];
rows_ = rhs.rows_;
cols_ = rhs.cols_;
}
// 拷贝数据
std::copy(rhs.data_, rhs.data_ + rows_ * cols_, data_);
return *this;
}
// 移动赋值 (C++11)
Matrix& operator=(Matrix&& rhs) noexcept {
if (this == &rhs) return *this;
delete[] data_;
data_ = rhs.data_;
rows_ = rhs.rows_;
cols_ = rhs.cols_;
rhs.data_ = nullptr;
rhs.rows_ = rhs.cols_ = 0;
return *this;
}
// 复合赋值运算符
Matrix& operator+=(const Matrix& rhs) {
assert(rows_ == rhs.rows_ && cols_ == rhs.cols_);
for (int i = 0; i < rows_ * cols_; ++i) {
data_[i] += rhs.data_[i];
}
return *this;
}
Matrix& operator*=(double scalar) {
for (int i = 0; i < rows_ * cols_; ++i) {
data_[i] *= scalar;
}
return *this;
}
// 使用示例
friend Matrix operator+(Matrix lhs, const Matrix& rhs) {
lhs += rhs; // 利用 += 实现 +
return lhs;
}
private:
int rows_, cols_;
double* data_;
};
// 使用
Matrix A(3, 3), B(3, 3), C(3, 3);
A = B = C; // 连锁赋值
A += B += C; // 连锁复合赋值
A = (B += C) * 2.0; // 混合运算
6.2 字符串类(简化版 std::string)
cpp
class MyString {
public:
MyString(const char* str = "") {
size_ = strlen(str);
data_ = new char[size_ + 1];
strcpy(data_, str);
}
MyString& operator=(const MyString& rhs) {
if (this == &rhs) return *this;
delete[] data_;
size_ = rhs.size_;
data_ = new char[size_ + 1];
strcpy(data_, rhs.data_);
return *this;
}
MyString& operator+=(const MyString& rhs) {
char* newData = new char[size_ + rhs.size_ + 1];
strcpy(newData, data_);
strcat(newData, rhs.data_);
delete[] data_;
data_ = newData;
size_ += rhs.size_;
return *this;
}
MyString& operator+=(const char* rhs) {
*this += MyString(rhs);
return *this;
}
private:
char* data_;
size_t size_;
};
// 使用
MyString s1, s2, s3;
s1 = s2 = s3 = "Hello";
s1 += s2 += " World"; // 连锁字符串拼接
6.3 配置对象链式设置
cpp
class Config {
public:
Config& operator=(const Config& rhs) = default;
// 利用返回引用来实现链式调用
Config& setHost(const std::string& host) {
host_ = host;
return *this;
}
Config& setPort(int port) {
port_ = port;
return *this;
}
Config& setTimeout(int ms) {
timeout_ = ms;
return *this;
}
private:
std::string host_;
int port_ = 80;
int timeout_ = 5000;
};
// 使用
Config cfg;
cfg.setHost("localhost")
.setPort(8080)
.setTimeout(10000); // 优雅的链式调用
七、自赋值检查
在实现 operator= 时,除了返回 *this,还需要注意自赋值检查:
cpp
class Widget {
public:
Widget& operator=(const Widget& rhs) {
if (this == &rhs) { // 自赋值检查
return *this;
}
// 释放原有资源
delete data_;
// 分配新资源并拷贝
data_ = new int(*rhs.data_);
return *this;
}
private:
int* data_;
};
如果没有自赋值检查:
cpp
Widget w;
w = w; // 自赋值!
执行过程:
delete data_------ 释放了自己的内存*rhs.data_------ 访问已释放的内存!未定义行为!
八、现代 C++ 的补充
8.1 拷贝并交换(Copy-and-Swap)惯用法
现代 C++ 推荐的一种更安全的实现方式:
cpp
class Widget {
public:
Widget& operator=(Widget rhs) { // 传值,利用拷贝构造
swap(rhs); // 与临时对象交换
return *this;
}
void swap(Widget& other) noexcept {
using std::swap;
swap(data_, other.data_);
}
private:
int* data_;
};
优点:
- 自动处理自赋值(如果
rhs是自身的拷贝,交换也无害) - 异常安全(如果拷贝构造失败,原对象不受影响)
- 代码简洁
8.2 默认赋值运算符
对于简单的类,可以让编译器自动生成:
cpp
class Point {
public:
Point(int x, int y) : x_(x), y_(y) {}
// 编译器生成的默认赋值运算符就很好
Point& operator=(const Point&) = default;
private:
int x_, y_;
};
九、总结
| 返回类型 | 支持连锁赋值 | 支持 (a=b)=c | 效率 | 推荐度 |
|---|---|---|---|---|
void |
否 | 否 | N/A | 不推荐 |
Widget(值) |
是 | 是 | 低(有拷贝) | 不推荐 |
const Widget& |
是 | 否 | 高 | 不推荐 |
Widget& |
是 | 是 | 高 | 强烈推荐 |
请记住:
- 令赋值操作符返回一个 reference to *this。
- 这条规则适用于所有赋值相关运算符:
=、+=、-=、*=、/=等。- 返回引用可以支持连锁赋值,且避免了不必要的拷贝开销。
- 返回非 const 引用保持了与内置类型行为的一致性。
- 不要忘记自赋值检查,或考虑使用拷贝并交换惯用法。
operator= 返回 *this 的引用,这个小小的约定让 C++ 的赋值操作变得优雅而高效。它是 C++ 运算符重载中最基础也最重要的惯用法之一,值得每一位 C++ 开发者牢记于心。
参考阅读:
- 《Effective C++》第三版,Scott Meyers
- 《C++ Primer》第五版,关于运算符重载的章节
- C++ Core Guidelines: C.60, C.61, C.62, C.63