C++ / a working model

100 / 103   ·   C++20   ·   约 12 分钟

Traits 适配的是表达式,Concepts 检查的是边界

先记住这句话

同一个泛型算法可以接纳字段和成员函数不同的类型,只要 traits 提供一致操作。用 C++20 requires 把错误挡在接口处,并区分语法满足、返回类型与业务意义这三层承诺。

本篇内容
  1. 先说明算法真正需要什么
  2. 用 requires 约束调用者可观察的结果
  3. 让历史技巧帮助阅读,而非增加隐蔽性
  4. 运行示例
  5. 动手练习
READING EVIDENCE / 已读部分正文

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 已证明范围正确;如果业务保证不可能为负,也要让这个前置条件可见。

继续查证

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

回到目录