Item10–令赋值操作符返回一个&

1.令赋值(assignment)操作符返回一个&

1. 核心目的

是为了实现 “连锁赋值”(Chained Assignment)

在 C++ 中,对于内置类型(如 int),我们习惯这样写代码:

int x, y, z;
x = y = z = 15; // 连锁赋值

由于赋值运算符是右结合(right-associative)的,上面的代码解析为: x = (y = (z = 15)); 为了让自定义的类也能支持这种写法,你的 operator= 必须返回一个指向对象本身的引用。

2. 标准写法示例

这是一个“协议”(Convention),虽然不是强制的语法要求,但所有的标准库类型(如 std::string, std::vector)都遵守这个协议。

class Widget {
public:
    // 1. 返回类型必须是当前类的引用 (Widget&)
    Widget& operator=(const Widget& rhs) {
        
        // ...这里执行赋值操作(如拷贝数据)...

        // 2. 返回 *this (即当前对象的引用)
        return *this;
    }
};

3. 使用场景演示

如果你遵守了 Item 10,用户就可以这样写:

Widget w1, w2, w3;
// ... 初始化 w3 ...

w1 = w2 = w3; 
// 过程:
// 1. w2.operator=(w3) 被调用,返回 w2 的引用。
// 2. w1.operator=(w2) 被调用(参数是上一步返回的 w2),返回 w1 的引用。

4. 扩展适用范围

这个规则不仅适用于标准的赋值运算符 =,也适用于所有赋值相关的运算(compound assignment operators):

  • +=

  • -=

  • *=

  • /=

class Widget {
public:
    Widget& operator+=(const Widget& rhs) {
        // ... 执行加法 ...
        return *this; // 依然要返回 *this
    }
};

5. 如果不遵守会怎样?

  • 如果返回 void:代码编译可能没问题,但用户无法使用 a = b = c 这样的写法,这会让你的类显得“格格不入”,不符合 C++ 直觉。

  • 如果返回对象(By Value):虽然支持连锁赋值,但会导致不必要的拷贝构造(Copy Constructor)调用,严重影响性能。

  • 如果返回 const reference:虽然避免了拷贝,但会阻止某些合法的(虽然少见)操作,例如 (a = b).func() 可能会因为 const 限制而失败。

总结

这就是一个单纯的“从众”原则:既然 int 这样做,既然标准库这样做,为了让你的类好用且符合直觉,你也应该这样做。

记住模版:

ClassName& operator=(const ClassName& rhs) {
    // ...
    return *this;
}

2.如果返回 `const reference‘的问题

1. 实际代码限制:链式调用与立即修改

假设我们有一个类 Widget,并且它的赋值操作符返回 const Widget&

C++

class Widget {
public:
    // 【错误示范】返回 const 引用
    const Widget& operator=(const Widget& rhs) {
        // ... 赋值逻辑 ...
        return *this; 
    }

    // 一个普通的非 const 成员函数
    void setup() { 
        // 做一些初始化工作 
    }
};

场景一:赋值后立即调用方法 (Method Chaining)

这是一种比较常见的写法,尤其是在需要对赋值后的对象立即进行某些操作时。

C++

Widget w1, w2;

// 意图:把 w2 赋给 w1,然后立即初始化 w1
(w1 = w2).setup(); 

// ❌ 编译报错!
// 原因:
// 1. (w1 = w2) 返回的是 const Widget&(只读引用)。
// 2. setup() 是一个非 const 函数。
// 3. C++ 禁止在 const 对象上调用非 const 函数。

如果 operator= 返回的是普通的 Widget&,这行代码是完全合法的。

场景二:作为非 const 函数的参数

假设有一个函数需要修改传入的对象:

C++

void logAndModify(Widget& w); // 接受非 const 引用

Widget a, b;
// 意图:把 b 赋给 a,然后把 a 传进去处理
logAndModify(a = b); 

// ❌ 编译报错!
// 原因:logAndModify 需要 Widget&,但赋值表达式提供的是 const Widget&。

2. 与内置类型 (int) 的行为对比

这是 Scott Meyers 在《Effective C++》中反复强调的原则:“Do as the ints do”(像 int 那样行事)。

让我们看看 int 是怎么处理这种“奇怪”操作的:

C++

int x, y, z;
x = 0;
y = 1;
z = 2;

// 写法:(x = y) = z;
// 意思是:先把 y 赋给 x,然后把 z 赋给 x。
// 最终结果:x 等于 2,y 等于 1,z 等于 2。
(x = y) = z;

这段代码在 C++ 中是合法的(虽然逻辑上有点奇怪,但在某些宏定义或特定的算法模板中可能会出现)。

  • x = y 返回的是 x 的引用(可修改的左值)。

  • 然后 x 被再次赋值为 z

如果你的类返回 const reference

C++

Widget w1, w2, w3;
(w1 = w2) = w3; 

// ❌ 编译报错!
// 原因:(w1 = w2) 返回的是 const 引用,你不能给一个 const 对象赋值。

总结

虽然 (a = b) = c 这种写法在实际业务代码中写出来可能会被同事打,但 (a = b).func() 这种写法是有实际应用场景的。

为了保证你的类:

  1. 灵活:支持连式调用和后续修改。

  2. 一致:表现得像内置类型,不让使用者感到意外(Principle of Least Astonishment)。

标准做法始终是:返回 T&(non-const reference)。

发表评论