没有实测,下面的内容来自标准规则和实现知识。

一、是什么

  • concept(概念):给模板参数的要求起一个名字,本质是一个编译期的布尔谓词。
  • requires 子句:把这个谓词挂到模板上,不满足就不参与重载。
  • requires 表达式:检查"某些表达式对这个类型是否合法",求值结果是 truefalse
template <class T>
concept Hashable = requires(T a) {
    { std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};

一个 concept 可以用四种写法挂到函数上,效果相同:

template <Hashable T> void f(T);                     // ① 直接作为类型约束
template <class T> requires Hashable<T> void f(T);   // ② requires 子句
template <class T> void f(T) requires Hashable<T>;   // ③ 尾置 requires
void f(Hashable auto x);                             // ④ 简写函数模板

二、解决了什么问题

以前(SFINAE、enable_if 现在(concept)
template <class T, std::enable_if_t<std::is_integral_v<T>, int> = 0> template <std::integral T>
报错发生在模板实例化的深处,错误信息几百行 在调用处报错:"约束不满足,因为某个表达式不合法"
对模板的要求只能写在注释里 要求就写在声明上,本身就是接口文档
两个 enable_if 重载的条件有重叠时产生二义性,需要手动写互斥条件 子句包含(subsumption):约束更严格的重载自动胜出
用 tag dispatch 按能力分派(比如 iterator_category 直接写几个受约束的重载

auto 也可以加约束:std::integral auto n = count(); 读代码的人一眼就能看出 n 是个整数。

三、requires 表达式的四种要求

template <class T>
concept Container = requires(T c, const T cc) {
    c.begin(); c.end();                                  // ① 简单要求:表达式合法即可
    typename T::value_type;                              // ② 类型要求:这个类型存在
    { cc.size() } noexcept -> std::same_as<typename T::size_type>;
                                                         // ③ 复合要求:表达式合法、不抛异常、返回类型满足约束
    requires sizeof(typename T::value_type) <= 64;       // ④ 嵌套要求:真正求值一个布尔条件
};

四、子句包含:按约束的严格程度选择重载

template <class T> concept Animal = requires(T t) { t.eat(); };
template <class T> concept Dog    = Animal<T> && requires(T t) { t.bark(); };

void f(Animal auto) { /* #1 */ }
void f(Dog auto)    { /* #2 */ }   // Dog 包含了 Animal,更严格

f(dog);   // 两个都满足,选 #2,不会二义
f(cat);   // 只满足 Animal,选 #1

标准库的 std::advance 就可以这样写:给 random_access_iterator 写一个 O(1) 的版本,给 bidirectional_iterator 写一个逐步前进的版本,编译器会自动选中最具体的那个。

五、使用场景

场景 例子
泛型算法库 std::ranges 的所有算法都带约束。std::ranges::sort(list) 会直接报"不满足 random_access_range",不再是模板内部几百行的错误
按能力分派重载 迭代器分类、"可序列化"和"不可序列化"走不同路径,取代 tag dispatch 和 enable_if
约束数值模板 template <std::floating_point T> T lerp(T a, T b, T t);
约束回调参数 void on_event(std::invocable<const Event&> auto&& cb);std::predicate<T>
类模板里按条件提供成员函数 void print() const requires Printable<T>; 只有 T 可打印时才有这个成员
按条件平凡的特殊成员函数 std::optional 的实现:~optional() requires std::is_trivially_destructible_v<T> = default; 和一个非平凡的 ~optional() 并存,于是 optional<int> 仍然是平凡可析构的。以前要靠多层基类技巧才能做到
在函数内部检测能力 if constexpr (requires { c.reserve(n); }) c.reserve(n); 容器支持 reserve 才调用
确认自己写的类型满足接口 static_assert(std::ranges::random_access_range<MyVec>);,可以取代 CRTP 里手写的接口检查
标准库的其他地方 std::formattable(C++23)、std::regularstd::totally_ordered

常用的标准 concept:

  • <concepts>same_asderived_fromconvertible_tointegralfloating_pointmovablecopyableregularequality_comparabletotally_orderedinvocablepredicate
  • <iterator>input_iteratorforward_iteratorrandom_access_iteratorcontiguous_iteratorsentinel_for
  • <ranges>rangesized_rangeviewrandom_access_rangecontiguous_range

六、常见坑

1. 在简单要求里写布尔条件,永远为真

template <class T>
concept Small = requires {
    sizeof(T) <= 8;           // 错:这只检查"这个表达式能不能编译",而它总能编译
    requires sizeof(T) <= 8;  // 对:嵌套要求才会真正求值
};

2. 复合要求检查的是 decltype((expr)),要注意引用

{ c.front() } -> std::same_as<typename T::value_type>;    // 失败:front() 返回的是 value_type&
{ c.front() } -> std::convertible_to<typename T::value_type>;   // 通常应该这样写

3. 子句包含只认命名的 concept

template <class T> requires std::is_integral_v<T>                       void g(T);  // #1
template <class T> requires std::is_integral_v<T> && std::is_signed_v<T> void g(T);  // #2
g(1);   // OK:#2 包含 #1

template <class T> requires requires(T t) { t.eat(); }                         void h(T);
template <class T> requires requires(T t) { t.eat(); } && requires(T t) { t.bark(); } void h(T);
h(dog); // 二义:两个 requires 表达式即使写得一模一样,也被当成不同的原子约束

包含关系只在"约束来自同一个出处"时成立,也就是引用同一个 concept 或同一个表达式。所以要把约束提取成命名的 concept 再组合。另外,std::integral<T>std::is_integral_v<T> 之间不存在包含关系。

4. concept 只检查语法,不检查语义,也不检查模板函数体

  • std::regular 要求"相等比较满足等价关系",但编译器只能检查 == 能不能编译,语义要求只是文档约定。
  • 函数体里用到了 concept 没有要求的操作,不会在定义处报错,而是到实例化时才报错。这和 Rust trait 的"定义时检查"不同。C++0x 最早的 concepts 设计是有定义检查的,2009 年被整体撤下,C++20 版本没有这个功能。

5. 访问权限按 concept 所在的上下文检查

requires 表达式里访问一个 private 成员,结果是"不满足"(false),不会报错,friend 声明也帮不上忙。这可能导致重载悄悄选中另一个版本。

6. 满足性结果在同一个编译单元里只算一次

对一个不完整类型检查某个 concept,之后类型变完整了,或者后来又加了一个重载,同一个 concept 对这个类型的结果可能改变。这时程序是 ill-formed, no diagnostic required(不要求编译器报错)。

7. requires requires

template <class T> requires requires(T t) { t.foo(); } void f(T);

第一个 requires 是子句,第二个是表达式。这样写合法,但可读性很差,应该提取成一个命名的 concept。

七、面试速答

  • concept 是什么? 给模板参数的要求起的名字,是一个编译期谓词。不满足的模板不参与重载,报错直接指向调用处。
  • 比 SFINAE 好在哪? 可读,报错清晰,而且能按约束的严格程度自动选择重载,这是 enable_if 做不到的。
  • requires 有哪几种用法? requires 子句用来挂约束;requires 表达式用来检查表达式是否合法,里面有简单、类型、复合、嵌套四种要求。
  • concept 会检查语义吗? 不会,只检查语法层面的合法性,也不检查模板函数体。
  • 有什么典型的坑? 在简单要求里写布尔条件永远为真,必须用嵌套的 requires;子句包含只认命名的 concept。