C++ / a working model

101 / 163   ·   C++11   ·   约 9 分钟

从设计约束出发:用值类型守住区间不变量

先记住这句话

D&E第1章把表达能力、运行效率与工具可获得性放在同一个工程问题中。本课据已读英文样章设计一个小型区间值类型:不依赖继承,也不追求语法炫技,而是在构造边界建立可验证的不变量。

本篇内容
  1. 类首先是概念边界,不是继承节点
  2. 明确正常结果与拒绝输入
  3. 把性能结论留给实际环境
  4. 运行示例
  5. 动手练习
READING EVIDENCE / 已读完整正文

The Design and Evolution of C++

全部18章实质正文已实际阅读:第1章取自出版社英文样章印刷页19–25;第2–18章取自2002英文重印扫描,逐页连续覆盖PDF38–431(包括分部页,扫描省略的空白页不计正文)。远程OCR文字与代码逐段阅读,识别不全的代码、表格和继承/RTTI图以原图补核;末段PDF387–431直接读图。full指18章正文完成,不声称逐字校勘或读完书末参考文献、索引。该重印收入1995等后续补订,不冒充未经修订的1994首印;中译本未计入覆盖。

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

类首先是概念边界,不是继承节点

读过的第1章描述了一种张力:能够直接表达应用概念的类型系统有助于设计,但工具的链接、运行与可移植性问题同样能决定项目成败。本课不把历史经历变成语言排行榜,而把它转成一个小问题:如何保证整数区间始终有效?

若两个边界在程序各处独立传递,每个调用者都必须记住左端不能大于右端。把它们放进一个类型,并让构造函数检查关系,便把这条约束集中在对象入口。私有成员防止外部绕过检查修改某一端;这里不需要基类、虚函数或堆对象。

明确正常结果与拒绝输入

例子采用半开区间 [first,last),因此空区间是合法状态,右端不属于区间。contains只比较整数,不计算端点之差,也就不需要额外处理有符号减法溢出。构造时发现逆序立即抛出异常,而不是暗中交换端点;交换会掩盖调用者把参数传反的问题。

一个成功构造的对象总是满足不变量。构造失败则没有可交给用户的完整对象,这与先制造无效对象、再要求用户调用修复函数不同。断言用于例子的行为检查;输入检查使用实际分支,不能因为关闭断言就消失。

把性能结论留给实际环境

封装并不要求增加动态分配,本例对象的状态只有两个整数。但这不等于承诺所有编译器上类大小必然等于两个整数之和,也不承诺函数一定内联。标准规定可观察的语义,具体布局和机器指令由实现决定。

在C++20项目中仍可使用这一设计,只有需求确实增加时才增加接口。例如加入宽度操作时,必须重新审视整数范围,而不能因为当前比较安全就认为所有算术都安全。这是从约束到机制的推导,不是把书中的早期代码原样升级。本课主题取自第1章;其他章节的实际阅读进度独立记录在书目覆盖说明中,不能由这一节课推断已经读完全书。

常见误区

  • 不要用assert代替对外部参数的运行时校验;定义NDEBUG后assert可能被移除。
  • 半开区间允许first等于last;不要把空区间误判为构造失败,也不要直接用int相减求任意端点的宽度。

运行一个例子

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

#include <cassert>
#include <iostream>
#include <stdexcept>

class Interval {
    int first_;
    int last_;
public:
    Interval(int first, int last) : first_(first), last_(last) {
        if (first > last) throw std::invalid_argument("reversed interval");
    }
    bool contains(int value) const {
        return first_ <= value && value < last_;
    }
};

int main() {
    const Interval work(3, 7);
    assert(work.contains(3));
    assert(!work.contains(7));
    const Interval empty(4, 4);
    assert(!empty.contains(4));
    bool rejected = false;
    try {
        const Interval invalid(7, 3);
        (void)invalid;
    } catch (const std::invalid_argument&) {
        rejected = true;
    }
    assert(rejected);
    std::cout << "interval invariant preserved\n";
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-design-and-evolution.cpp -o example && ./example

预期结果

interval invariant preserved

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

增加empty()成员函数,并说明为什么不应该用last_-first_==0实现它。

查看参考答案

加入bool empty() const { return first_ == last_; }即可。端点相等正好表达半开区间为空,不需要算术。减法会引入不必要的整数范围推理;例如合法区间的端点可能分别接近int最小值和最大值,求差可能溢出,而相等比较始终安全。

继续查证

标准草案与官方章节会更新;版本标记只说明示例最低要求。

回到目录