97 / 103 · C++20 · 约 14 分钟
Policy-based design:分离决定,不复制流程
同一个容量规则可以配合不同的超限处理方式。用成员组合与 C++20 concepts 写一个小型策略宿主,区分结构兼容、语义承诺,以及编译期配置和运行时替换。
Modern C++ Design: Generic Programming and Design Patterns Applied
已完整阅读 2001 年第一版第 1–11 章及附录 A 的可读正文和文本代码:策略、技术、typelist、small-object allocator、广义函子、singleton、智能指针、factory、abstract factory、visitor、multimethods 与线程讨论。私有全文提取共 16122 行,连续分段阅读并补读截断区间 216–554;另读出版社第 1、7、8、11 章用于交叉核对。书中 C++98 和平台线程技巧按历史材料理解,不当作 C++20 推荐实现。
查看版本、实际阅读范围与原文入口 →先找稳定流程与真实变化点
一个批次接收接口允许至多 cap 个元素。容量比较是稳定规则,超限之后报错还是只接收 cap 个元素则是可替换决定。若为两个决定各复制一份完整类,修复容量逻辑时就容易只更新其中一份。宿主 BoundedBatch 应只负责比较,超限时把输入和上限交给策略。
这对应书中宿主与策略的分工,但本课不需要继承。策略作为成员保存,外部没有理由把 BoundedBatch 当作 RejectOverflow 使用;组合也避免暴露一套与业务对象身份无关的基类接口。只有确实需要扩展的决定才成为参数,不把所有语句都拆成策略。
concept 检查表达式,不证明业务语义
OverflowPolicy 要求 const 策略对象可以用两个 size_t 调用,并返回 size_t。这个约束把诊断移到接口入口,比等到深层模板实例化才发现没有调用运算符更清楚。不过返回一个比 cap 更大的值仍能满足语法约束,所以 concepts 不是定理证明器。
两个策略分别实现抛异常和截断。宿主对策略还有语义约定:正常返回时不得超过上限;不接受请求可以抛异常。示例用同一组正常输入和超限输入观察这两种行为,而不是只证明模板能实例化。状态策略也可被成员保存,但那会新增状态与复制契约,需要另行说明。
静态配置有明确适用范围
RejectOverflow 与 ClampOverflow 产生不同宿主类型,调用绑定在编译期完成。它适合程序构建时确定规则的场景;若用户在运行中切换策略,普通分支、variant 或虚接口可能更直接,不能为了“零开销”强迫调用者重新组织整个系统。模板实例也可能增加代码体积,应以实际需求决定是否值得。
2001 年的 Modern 不是今天所有安全写法的同义词。书中的 auto_ptr、定制复制语义和指针转换是历史讨论,现代资源管理优先使用标准智能指针。本例只继承策略分解思想,使用 C++20 约束和成员组合;它既不替换标准容器,也不鼓励重写一套通用对象框架。
容易答错的地方
- concept 只能检查表达式约束;返回值是否遵守容量上限仍是策略的语义契约。
- 把本应相互协调的两项决定强行拆成独立策略,会隐藏非法组合而非消除耦合。
运行一个例子
最低标准 C++20 · 完整程序 · 下载 .cpp
#include <cassert>
#include <concepts>
#include <cstddef>
#include <stdexcept>
struct RejectOverflow {
std::size_t operator()(std::size_t, std::size_t) const {
throw std::length_error("batch exceeds limit");
}
};
struct ClampOverflow {
std::size_t operator()(std::size_t, std::size_t cap) const {
return cap;
}
};
template<class P>
concept OverflowPolicy = requires(const P& p, std::size_t n) {
{ p(n, n) } -> std::same_as<std::size_t>;
};
template<OverflowPolicy P>
class BoundedBatch {
std::size_t cap_;
P policy_{};
public:
explicit BoundedBatch(std::size_t cap) : cap_(cap) {}
std::size_t accepted(std::size_t requested) const {
return requested <= cap_ ? requested : policy_(requested, cap_);
}
};
int main() {
const BoundedBatch<RejectOverflow> strict{5};
const BoundedBatch<ClampOverflow> partial{5};
assert(strict.accepted(4) == 4);
assert(partial.accepted(4) == 4);
assert(partial.accepted(8) == 5);
bool rejected = false;
try { (void)strict.accepted(8); }
catch (const std::length_error&) { rejected = true; }
assert(rejected);
}
在本地编译
g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread books-modern-cpp-design.cpp -o example && ./example预期结果
预期:正常退出、无输出;所有 assert 通过。
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
新增一种超限时完全不接收的策略,需要修改 BoundedBatch 吗?
查看参考答案
不需要。定义 struct DropOverflow { std::size_t operator()(std::size_t, std::size_t) const { return 0; } };,再实例化 BoundedBatch<DropOverflow>{5}。请求 4 仍返回 4,请求 8 返回 0。它遵守“不超过容量”的共同契约,因此无需改变宿主比较流程。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。