Effective C++ 条款10:令 operator= 返回一个 reference to *this

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;

执行过程:

  1. w2 = w3 返回 Widget 的临时拷贝
  2. w1 = (临时拷贝) 再次赋值
  3. 临时拷贝被销毁

如果 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;  // 自赋值!

执行过程:

  1. delete data_ ------ 释放了自己的内存
  2. *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
相关推荐
摇滚侠2 小时前
JavaSE 和 JavaEE 是什么意思
java·java-ee
想带你从多云到转晴2 小时前
03、JAVAEE---多线程(三)
java
某林2122 小时前
Isaac Sim 5.1.0 无头服务器部署与 RTX 显存段错误排障全记录
运维·服务器·docker·容器·isaac
王老师青少年编程2 小时前
2026年全国青少年信息素养大赛算法应用主题赛(C++赛项-复赛模拟卷6:文末附答案)
c++·答案·模拟卷·复赛·2026年·青少年信息素养大赛·算法应用主题赛
满怀冰雪2 小时前
第04篇-双指针算法-从有序数组到回文判断的高频解法
java·算法
matlabgoodboy2 小时前
计算机java程序代写python代码编写c/c++代做qt设计php开发matlab
java·c语言·python
|_⊙2 小时前
Linux 中断
linux
m0_738120722 小时前
Docker 环境下 Vulfocus 靶场搭建全流程(附镜像源问题解决方案)
运维·服务器·网络·安全·docker·容器
leo__5202 小时前
MATLAB实现牧羊人算法
开发语言·算法·matlab