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验证接口
赋值结果实验
结果值、结果身份与状态
先预测赋值表达式会返回谁,再切换场景检查引用类型、地址和异常证据。
表达式
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;赋值运算符是
↡多个同优先级赋值表达式从右向左分组,例如 x = (y = z) 的语法规则。。
先执行 y = z,其结果代表更新后的 y,再作为 x = ... 的右操作数。
让用户定义类型保持相同惯例。
标准签名与最小实现
↡从同类型 const 左值源复制状态到已存在目标对象的成员操作符。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 仍要正确
↡赋值左右操作数最终指向同一对象,例如 `a = a` 或别名间接形成的情况。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& 的惯例,只收紧调用对象。
标准泛型约束期待相同语义
↡标准 concept,要求目标左值可由源赋值且结果类型为目标左值引用,并满足共同引用语义。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 返回,可能不满足泛型约束,限制标准算法互操作。
返回身份与状态的双重测试
↡验证 assignment expression 的地址、类型和值都符合契约的一组测试。先预测:(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_from与same_as可在编译期验证泛型接口
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 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:比较三种返回(assignment operators return a reference to this、assignment chaining、operator=)。 分析 operator= 返回
T、const T&、T&对身份、连锁和泛型 concept 的影响。
- *问题 2:实现安全赋值(reference to this、copy-and-swap assignment、assignment exception guarantee)。 一个 Document 有多个可能抛异常的成员,写出强保证且返回契约正确的 operator=。
- 问题 3:验证跨类型与 ref-qualifier(operator=、lvalue-qualified assignment、assignable_from)。 Text 接收 string_view 赋值,设计 alias、临时目标和 concept 测试。