92 / 103 · C++11 · 约 12 分钟
多态接口不暴露赋值:让叶子保持完整值语义
通过基类引用赋值通常只改变基类子对象,不代表完整的多态复制。把接口层赋值设为受保护,让具体叶子使用正常拷贝,以可调用性检查证明客户端不能跨越这个边界。
More Effective C++: 35 New Ways to Improve Your Programs and Designs
已通读公开267页中文合编PDF的全部技术条目Items1–35(PDF11–240)、推荐读物与auto_ptr附录(240–247)、两篇附加文章(247–267),并直接读图核对正文全部31幅嵌入图及第9页记号说明。full仅指这一公开中文文本的全部实质章节,不是英文原版或商业中文版逐页核验:商业译本版次未确认;前置4–5页中文在源PDF及图像均为方框,未恢复,也不计技术正文。另读过出版社英文Item33交叉核对;历史译注与代码错误不当作C++20规则。
查看版本、实际阅读范围与原文入口 →先问复制在业务中意味着什么
Item 33 从部分赋值讨论非叶子类设计:两个对象经由基类引用相遇时,静态可见的赋值函数通常只能处理基类状态。即使对象动态类型相同,也不能据此断言全部派生成员已经更新。强行把赋值声明为虚函数,又会引出不同派生类型能否互相赋值的问题。
本例中的规则接口只负责回答一个金额,不承诺通过接口替换整个规则。具体固定规则保存自己的数值;复制两个固定规则是合理操作,但通过抽象接口赋值没有业务含义。因此从接口上排除它,比在运行时猜测两端类型更明确。
受保护不是删除所有复制能力
基类的默认构造、拷贝构造和拷贝赋值放在 protected 区域。这样派生类仍能生成正常的拷贝操作,外部调用者却不能直接执行基类赋值。基类保留公开虚析构,允许将来通过拥有型基类指针销毁对象;纯虚查询使接口本身不可实例化。
具体叶子使用 final 表示这里不再承诺扩展继承。它并不禁止复制,也不替代访问控制。示例先在叶子类型上完成赋值,再经由 const 基类引用观察结果,验证更新的是完整具体值,而不是试图复现危险的部分更新。
用边界检查表达禁止,而不运行坏代码
两个 static_assert 分别验证接口不可赋值、具体类型可赋值。is_assignable 从与这两个类型无关的上下文检查表达式有效性,因此会考虑 protected 权限。这比注释一句不要这样用更可靠,也不需要把故意编译失败的语句混进可运行程序。
这不是要求所有非叶子类都机械改成抽象类。若现有模型确实需要多态复制,应单独设计明确返回新对象的接口,并写明所有权与异常保证。当前需求没有这项能力,就不添加克隆框架。书中建议的价值在于让设计暴露真实语义,而不是增加层数。
容易答错的地方
- 仅把派生类标成 final,不会阻止调用者通过一个仍公开可赋值的基类接口实施部分赋值。
- 将基类赋值直接 = delete 会影响派生类隐式生成的赋值;本例选择 protected default,保留叶子的正常值语义。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <type_traits>
class Rule {
protected:
Rule() = default;
Rule(const Rule&) = default;
Rule& operator=(const Rule&) = default;
public:
virtual ~Rule() = default;
virtual int amount() const noexcept = 0;
};
class FixedRule final : public Rule {
int amount_;
public:
explicit FixedRule(int amount) : amount_(amount) {}
int amount() const noexcept override { return amount_; }
};
static_assert(!std::is_assignable<Rule&, const Rule&>::value,
"The interface does not support assignment");
static_assert(std::is_copy_assignable<FixedRule>::value,
"Concrete values remain assignable");
int main() {
FixedRule first(7);
const FixedRule second(19);
first = second;
const Rule& view = first;
assert(view.amount() == 19);
assert(second.amount() == 19);
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-more-effective-cpp.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
如果把 Rule::operator= 改成公开 default,会改变哪一个编译期检查?
查看参考答案
is_assignable<Rule&, const Rule&>::value 会变成 true,即使 Rule 仍然是抽象类。抽象性阻止创建独立 Rule 对象,却不阻止通过指向现有派生对象的 Rule 引用执行公开赋值。因此第一个 static_assert 会失败,揭示接口边界被扩大。
继续查证
- More Effective C++ Item 33 — publisher full item
- C++ draft [class.copy.assign]
- C++ draft [meta.unary.prop] — is_assignable access checks
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。