100 / 103 · C++20 · 约 12 分钟
Traits 适配的是表达式,Concepts 检查的是边界
同一个泛型算法可以接纳字段和成员函数不同的类型,只要 traits 提供一致操作。用 C++20 requires 把错误挡在接口处,并区分语法满足、返回类型与业务意义这三层承诺。
Advanced C++ Metaprogramming
实际读完作者提供的 §5.2.2 Concept traits 两页样稿,以及 bonus chapter PDF 书页 405–408(§9.3.2 结尾及 §9.3.3 More on the double wrapper technique)。bonus 文件不是一整章,不据名称夸大范围。其余正文未取得;目录仅核对结构,零售和 Google Books 页面只有元数据,未充作正文。
查看版本、实际阅读范围与原文入口 →先说明算法真正需要什么
假设界面只想获得一个对象的记录数。一种外部类型保存 public 字段,另一种通过成员函数计算。若算法直接假定每个类型都有 count 成员,就把某个类的布局泄漏成所有类型的要求。traits 可以把这种差异压缩到适配层,为算法统一提供 get(value) 表达式。
示例故意不用继承改造两个类型。主模板只声明不定义,只有显式支持的类型才提供适配;未知类型不会因为碰巧存在一个同名字段就被默认接纳。这适合不能修改的外部结构或需要审查白名单的接口。如果项目里所有类型原本已经共享同一接口,则不必为了展示模板再加 traits。
用 requires 约束调用者可观察的结果
RecordCount 检查 count_traits<T>::get(value) 是否可以调用,并要求表达式结果精确为 size_t。这是在编译阶段验证接口形状,不会构造对象,也不会执行 get。算法 count_of 只接受满足该条件的类型,所以错误会集中到这个边界,而不是在多层模板内部暴露。
这里选择 same_as 而非 convertible_to 是明确的契约取舍:一个有符号整数虽然可以转成 size_t,却可能把负数变成巨大的正数。即使检查精确返回类型,也仍然不能证明值确实是记录数,更不能证明读取没有昂贵副作用。语义必须由适配实现和调用约定保证,概念不是程序验证器。
让历史技巧帮助阅读,而非增加隐蔽性
作者样稿强调 traits 规定语法而非唯一实体,相似写法可以由静态函数、构造或转换实现。这能帮助理解老式元编程为何常有多层包装,也提醒我们别把函数外观当成副作用保证。本例有意选择直白的静态函数,不借隐式转换完成资源释放或其他隐藏动作。
书中术语 concept traits 属于经典 C++ 的设计方式,并非后来 C++20 concepts 的语法。本课使用现代 requires 来表达约束,是基于样稿思想写出的新例子,不是声称原书介绍了 C++20。我们保留两个可观察结果与不支持类型的编译期断言,展示适配成功和边界拒绝,而不执行故意非法的代码。
容易答错的地方
- requires 检查表达式能否成立,不证明结果非负、准确、无副作用或线程安全。
- 不要以兜底 traits 猜测任意类型的同名字段含义;名称相同不等于业务契约相同。
运行一个例子
最低标准 C++20 · 完整程序 · 下载 .cpp
#include <cassert>
#include <concepts>
#include <cstddef>
#include <iostream>
struct Snapshot { std::size_t rows; };
class Batch {
std::size_t rows_;
public:
explicit Batch(std::size_t rows) : rows_(rows) {}
std::size_t size() const { return rows_; }
};
template<class T> struct count_traits;
template<> struct count_traits<Snapshot> {
static std::size_t get(const Snapshot& v) { return v.rows; }
};
template<> struct count_traits<Batch> {
static std::size_t get(const Batch& v) { return v.size(); }
};
template<class T>
concept RecordCount = requires(const T& v) {
{ count_traits<T>::get(v) } -> std::same_as<std::size_t>;
};
template<RecordCount T>
std::size_t count_of(const T& v) { return count_traits<T>::get(v); }
int main() {
static_assert(RecordCount<Snapshot>);
static_assert(RecordCount<Batch>);
static_assert(!RecordCount<int>);
Snapshot a{3};
Batch b{5};
assert(count_of(a) == 3);
assert(count_of(b) == 5);
std::cout << count_of(a) + count_of(b) << "\n";
}
在本地编译
g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread books-advanced-cpp-metaprogramming.cpp -o example && ./example预期结果
8
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
某个外部类型用 int 保存记录数,能否直接把它强转为 size_t 后当作适配结果?应如何处理负值?
查看参考答案
直接转换能通过本例概念,但负值会转换成很大的无符号值,破坏语义。适配层应先检查负值并抛出约定的异常,或另行设计返回 optional<size_t> 的可失败查询接口并同步调整概念。不能声称 same_as 已证明范围正确;如果业务保证不可能为负,也要让这个前置条件可见。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。