Item 10:让 assignment operators 返回 reference to *this

对齐 Effective C++ 第三版 Item 10:让 copy/move/compound assignment 返回非 const `*this` 引用,保持右结合连锁语义,并验证身份、异常安全和泛型约束。

学习目标

  • 能推导 x = y = z 的右结合求值与引用传播,证明每层 assignment expression 返回左操作数身份
  • 能实现 copy、move 与 compound assignment 的 reference-to-this 返回契约,同时保持自我赋值和异常安全
  • 能比较按值、const reference 与非 const reference 返回的后果,并使用 ref-qualifier 与 assignable_from 验证接口
Assignment result contractassignment chaining · operator= · reference to *thisassignment chainingx = (y = z)operator=return *thisresult identity&(a = b) == &a三条验收证据decltype → T&addressof → 左操作数exception → 目标不变返回契约保证身份;状态提交与异常安全仍需独立验证
赋值操作先按右结合完成源值传播,再由 operator= 返回左操作数自身;类型、地址和异常状态分别验收。

赋值结果实验

结果值、结果身份与状态

先预测赋值表达式会返回谁,再切换场景检查引用类型、地址和异常证据。

表达式

x = y = z

返回契约

right associativity 先执行 y = z;每个 assignment operator 都返回目标对象的非 const 左值引用。

当前场景 · assignment chaining

right associativity 先执行 y = z;每个 assignment operator 都返回目标对象的非 const 左值引用。

从“赋值为何可以连写”开始

C++ 内置类型支持:

int x{};
int y{};
int z{42};
x = y = z;

赋值运算符是

先执行 y = z,其结果代表更新后的 y,再作为 x = ... 的右操作数。

让用户定义类型保持相同惯例。

标准签名与最小实现

class Widget {
public:
    Widget& operator=(const Widget& rhs)
    {
        value_ = rhs.value_;
        name_ = rhs.name_;
        return *this;
    }
private:
    int value_{};
    std::string name_;
};

返回类型 Widget&return *this 对齐,返回对象身份与左操作数一致。

赋值结果可继续作为赋值目标或调用非 const 成员。

按值返回为何破坏惯例

class BadWidget {
public:
    BadWidget operator=(const BadWidget& rhs) // 不推荐
    {
        value_ = rhs.value_;
        return *this; // 复制出临时对象
    }
private:
    int value_{};
};

&(a = b) 不再等于 &a,后续操作可能作用在临时副本;还产生不必要复制/移动。

赋值惯例要求结果身份就是左操作数。

不要返回 const reference

旧式代码有时返回 const T&,试图阻止 (a = b) = c。这偏离内置类型和标准库惯例,也会阻止赋值结果调用非 const 成员或继续复合操作。

(a = b).normalize();
(a += delta) += anotherDelta;

若类型不希望某些操作,应在那些操作自身上设计约束,而不是削弱 operator= 的标准语义。

降低泛型代码和读者的意外。

复合赋值也返回 *this

class Vector2 {
public:
    Vector2& operator+=(const Vector2& rhs) noexcept
    {
        x_ += rhs.x_;
        y_ += rhs.y_;
        return *this;
    }
private:
    float x_{};
    float y_{};
};

惯例同样适用于 operator+=operator-=operator*= 以及自定义赋值形式。

move assignment 也返回目标引用

Buffer& Buffer::operator=(Buffer&& rhs) noexcept
{
    if (this != &rhs) {
        release();
        data_ = std::exchange(rhs.data_, nullptr);
        size_ = std::exchange(rhs.size_, 0);
    }
    return *this;
}

即使源状态变为 moved-from,结果仍是目标 *this。返回 Buffer&& 会把目标伪装成即将销毁的右值,不符合赋值惯例。

必须由 move 实现保证,与返回类型是两条独立责任。

返回 *this 不解决异常安全

Item 10 规定结果类型,不会自动保证多成员赋值原子性。

若先赋 name* 成功、再赋 payload* 抛异常,目标可能部分更新。可用 copy-and-swap:

class Document {
public:
    Document& operator=(Document rhs)
    {
        swap(rhs);
        return *this;
    }
 
    void swap(Document& other) noexcept;
};

按值参数统一 copy/move 来源,候选构造失败时 this 未变化;swap 后旧状态随 rhs 析构。

self-assignment 仍要正确

Item 11 会深入处理。Item 10 的边界是:无论是否 self-assignment,函数成功都返回同一个 this 引用。

Widget& result = (widget = widget);
assert(&result == &widget);

接口测试不能只覆盖字面同名。

跨类型 assignment 的返回类型

class Text {
public:
    Text& operator=(std::string_view value)
    {
        storage_.assign(value);
        return *this;
    }
private:
    std::string storage_;
};

仍返回目标 Text&,因为表达式结果代表左操作数,不代表源类型。

若 string_view 指向自身 storage 的一部分,assign 前要考虑 alias 与失效。

ref-qualifier 限制目标类别

赋值目标通常应是有持续身份的左值:

class Token {
public:
    Token& operator=(const Token&) & = default;
    Token& operator=(Token&&) & noexcept = default;
};

这不会改变返回 T& 的惯例,只收紧调用对象。

标准泛型约束期待相同语义

static_assert(std::assignable_from<Widget&, const Widget&>);
static_assert(std::same_as<
    decltype(std::declval<Widget&>() = std::declval<const Widget&>()),
    Widget&>);

若 operator= 按值或 const reference 返回,可能不满足泛型约束,限制标准算法互操作。

返回身份与状态的双重测试

先预测:(x = y) = z 若 operator= 返回值,会修改 x 还是临时副本?写出对象地址和最终值,再运行测试。

  • decltype(a = b) 精确为 T&
  • std::addressof(a = b) 等于 std::addressof(a)
  • x = y = z 后 x/y 都等于 z,z 不变。
  • (x += y) += z 两次都修改 x。
  • copy candidate 构造抛异常时 x 不变。
  • move assignment 后返回 x,源保持可析构/可赋值。

返回契约审查

Have assignment operators return a reference to *this(让 assignment operators 返回 *this 引用)应覆盖类型提供的整个赋值家族,而不只 copy assignment。逐项检查 move、标量赋值、+=-= 等操作:成功后是否返回非 const 目标左值引用,异常时是否没有伪造结果,重载之间是否保持同一身份语义。

审查调用点时同时看表达式类别和对象地址。auto result = (a = b) 会按值复制,即使 operator= 正确返回引用;若调用方要保留身份,应写 auto& result。这不是 operator 的缺陷,而是变量声明再次选择了值语义。测试应直接检查 decltype((a = b))std::addressof(a = b),避免被接收变量掩盖。

少数代理类型可能故意让 assignment expression 返回 proxy,例如压缩位容器的元素引用;这时左操作数本身就是 proxy,而返回仍应代表该目标语义。普通 class assignment 没有理由返回状态码或新副本;若赋值需要报告可恢复错误,应提供命名函数返回 expected,不要破坏 operator= 的惯例。

最后检查继承:operator= 不是 virtual,派生类的赋值应返回 Derived& 并完整复制 base/member。不要依赖 Base operator= 的返回自动恢复派生身份;Item 12 将继续验证复制所有组成部分。

小结

  • assignment 右结合,内层结果必须可作为外层源
  • copy/move/operator+= 等 assignment operators 应返回非 const T&
  • return *this 保持结果身份、避免临时副本并符合内置/标准库惯例
  • 返回契约不替代 self-assignment、资源和异常安全实现
  • ref-qualifier 可限制赋值目标为左值,但结果仍是目标引用
  • assignable_fromsame_as 可在编译期验证泛型接口

资料与写作方式声明

本章以Effective C++, Third Edition, Item 10权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

right associativity

赋值表达式从右向左分组的语法规则。

reference to *this

赋值后返回左操作数自身的非 const 引用。

copy assignment operator

从同类型 const 左值源复制到现有目标的操作。

this pointer

成员函数中指向当前调用对象的隐式指针。

lvalue result

标识有稳定身份与地址对象的表达式结果。

assignment-by-value return

赋值返回目标副本而非目标身份的设计。

result identity

函数结果与已存在对象共享地址和生命周期。

const assignment result

被顶层 const 限制的赋值返回引用。

conventional operator semantics

与内置和标准库操作保持一致的表达式语义。

compound assignment

修改左操作数的 +=、-= 等操作。

compound-assignment chaining

由返回引用连续修改同一左值的复合赋值。

move assignment operator

从右值源转移表示到现有目标的操作。

moved-from state

资源移出后仍可析构和赋值的源状态。

assignment exception guarantee

赋值失败时目标保持有效或值不变的保证。

copy-and-swap assignment

候选按值构造后以 no-throw swap 提交的赋值。

self-assignment

赋值左右最终引用同一对象的情况。

alias-induced self-assignment

不同表达式因别名指向同一对象形成的自赋值。

heterogeneous assignment

接受不同源类型并转换提交的赋值重载。

conversion assignment contract

跨类型赋值的转换、不变量和失败语义。

ref-qualifier

限制成员函数调用对象值类别的 & 或 &&。

lvalue-qualified assignment

只能对左值目标调用的赋值操作。

assignable_from

要求赋值结果为目标左值引用的标准 concept。

same_as

验证两个类型完全相同的标准 concept。

assignment-result test

检查赋值结果类型、地址与值的测试。

练习

  1. 问题 1:比较三种返回(assignment operators return a reference to this、assignment chaining、operator=)。 分析 operator= 返回 Tconst T&T& 对身份、连锁和泛型 concept 的影响。
  1. *问题 2:实现安全赋值(reference to this、copy-and-swap assignment、assignment exception guarantee)。 一个 Document 有多个可能抛异常的成员,写出强保证且返回契约正确的 operator=。
  1. 问题 3:验证跨类型与 ref-qualifier(operator=、lvalue-qualified assignment、assignable_from)。 Text 接收 string_view 赋值,设计 alias、临时目标和 concept 测试。

讨论

评论区加载中…