101 / 163 · C++11 · 约 9 分钟
从设计约束出发:用值类型守住区间不变量
D&E第1章把表达能力、运行效率与工具可获得性放在同一个工程问题中。本课据已读英文样章设计一个小型区间值类型:不依赖继承,也不追求语法炫技,而是在构造边界建立可验证的不变量。
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最小值和最大值,求差可能溢出,而相等比较始终安全。
继续查证
标准草案与官方章节会更新;版本标记只说明示例最低要求。