C++ / a working model

92 / 103   ·   C++11   ·   约 12 分钟

多态接口不暴露赋值:让叶子保持完整值语义

先记住这句话

通过基类引用赋值通常只改变基类子对象,不代表完整的多态复制。把接口层赋值设为受保护,让具体叶子使用正常拷贝,以可调用性检查证明客户端不能跨越这个边界。

本篇内容
  1. 先问复制在业务中意味着什么
  2. 受保护不是删除所有复制能力
  3. 用边界检查表达禁止,而不运行坏代码
  4. 运行示例
  5. 动手练习
READING EVIDENCE / 已读完整正文

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 会失败,揭示接口边界被扩大。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录