C++ / a working model

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

类不变量:让非法状态进不了对象

先记住这句话

构造函数不仅填成员,还负责建立对象承诺;修改操作必须维护同一承诺。以有容量上限的库存为例,把范围检查放进类型内部,并在失败前保持旧值不变,比要求每个调用者自觉检查更可靠。

本篇内容
  1. 先写规则,再写成员
  2. 失败前不修改:把一次操作做成事务
  3. 从教材手写容器迁移到现代代码
  4. 运行示例
  5. 动手练习
READING EVIDENCE / 已读完整正文

The C++ Programming Language

已分段通读第1—44章全部正文(印刷页3—1279),含代码、表格、章末建议,另读各版前言;私有文本L140—59048连续覆盖。随后直接核读126个已定位的原PDF图示、公式及上下文页,补齐对象布局、继承箭头、矩阵、流和并发等视觉内容。full指正文及已识别的实质图示读完,不表示逐页校勘全部1366个PDF页面;索引仅导航抽查,未逐条读。原书是C++11基线,C++20差异单列。

查看版本、实际阅读范围与原文入口 →

先写规则,再写成员

一个库存对象的承诺是 0 <= used <= capacity。如果把两个整数公开,调用者可以分别修改它们,任何一次赋值都可能制造非法组合。类的价值不在于把函数搬进大括号,而在于把合法状态和允许的变化集中到一个边界。

构造函数检查初始值;读取接口不改变状态;预订操作先检查剩余量再提交。这样,成功返回的构造和修改都维持同一个不变量。参数检查失败不是“库存为零”,而是没有产生新的合法操作结果,示例通过异常明确报告。

失败前不修改:把一次操作做成事务

预订时不要先加后检查。对有符号整数,先加可能已经溢出,随后检查来不及挽回;对无符号整数,回绕也会掩盖错误。已知不变量成立时,比较 amount > capacity - used 不会计算危险的总和。

只有检查通过才执行加法。对于这个只含整数的操作,异常路径保证旧状态完全不变;这不是所有成员函数自动享有的语言保证,而是我们安排语句顺序得到的接口性质。复杂资源修改也应先准备、后提交。

从教材手写容器迁移到现代代码

全书通过资源句柄、异常与特殊成员解释构造、析构和复制;它展示机制,不意味着业务代码应普遍手写动态数组。这个库存类没有资源所有权,所以无需声明析构或复制函数,默认值语义已经足够。

C++20 中仍适用这些设计原则。assert 用于示例检查,不替代面向外部输入的运行时验证;不能因为发布构建会关闭断言,就让非法参数悄悄进入对象。书中早期印次样例有作者勘误,本站程序不照搬其容器实现。

容易答错的地方

  • 先执行 used += amount 再判断上限,可能已发生整数溢出;必须先比较剩余容量。
  • 不要用断言处理来自外部的非法参数;断言可以被关闭,而接口承诺不能消失。

运行一个例子

最低标准 C++11 · 完整程序 · 下载 .cpp

#include <cassert>
#include <stdexcept>
class Stock {
    int capacity_;
    int used_;
public:
    Stock(int capacity, int used) : capacity_(capacity), used_(used) {
        if (capacity < 0 || used < 0 || used > capacity)
            throw std::invalid_argument("invalid stock");
    }
    int used() const noexcept { return used_; }
    void reserve(int amount) {
        if (amount < 0) throw std::invalid_argument("negative amount");
        if (amount > capacity_ - used_) throw std::out_of_range("no capacity");
        used_ += amount;
    }
};
int main() {
    Stock s(10, 3);
    s.reserve(7);
    assert(s.used() == 10);
    bool rejected = false;
    try { s.reserve(1); } catch (const std::out_of_range&) { rejected = true; }
    assert(rejected && s.used() == 10);
    rejected = false;
    try { Stock invalid(2, 3); } catch (const std::invalid_argument&) { rejected = true; }
    assert(rejected);
    Stock copy = s;
    copy.reserve(0);
    assert(copy.used() == s.used());
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-cpp-programming-language.cpp -o example && ./example

预期结果

预期:正常退出、无输出;所有 assert 通过。

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

增加 release(amount),保证释放过量失败且旧值不变。

查看参考答案

实现 if (amount < 0) throw std::invalid_argument("negative amount"); if (amount > used_) throw std::out_of_range("too much"); used_ -= amount;。在满额对象上 release(4) 后 used 为 6;再 release(7) 应抛异常且仍为 6。先比较后减法同时保证非负和失败不修改。

继续查证

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

回到目录