1 Concepts 简介
C++20 引入了 Concepts,这是一个革命性的特性,旨在解决模板编程中长期存在的问题,特别是编译错误信息晦涩难懂和对模板参数缺乏明确约束的问题。Concepts 提供了一种直接、清晰的方式来定义模板参数必须满足的接口要求。
在 C++20 之前,模板是“隐式契约”:
1 | template<typename T> |
如果传入一个不支持 +
的类型(如 std::thread
),编译器会报出一长串难以理解的错误信息,追溯到 operator+
调用失败。
Concepts 的目标就是将这种“隐式契约”变成“显式契约”:
1 | template<typename T> |
现在,如果 T
不满足 Addable
,编译器会直接告诉你“T
不满足 Addable
概念”,错误信息清晰明了。
2 如何定义Concept
2.1 一些定义 Concept 的案例
使用 template
和 concept
关键字,结合 requires
表达式来定义。
1 |
|
2.2 需求的语法
从前面的案例我们观察到, Concept
的定义中,我们使用了 requires
关键字,其后的块中我们定义了多个限制条件, 这些限制条件我们称为需求(requirement
), 其有下面几种语法:
2.2.1 简单需求 (Simple Requirements)
语法:
1 | expression; |
作用:
检查 expression
是否是一个语法上合法的表达式。它只关心表达式能否被正确解析和编译,而不关心其返回类型、值类别或语义。
示例:
1 | template<typename T> |
- 如果
T
定义了operator+
,这个需求就满足。 - 它不关心
a + b
返回的是T
、T&
、int
还是其他任何类型,只要表达式能写出来就行。
2.2.2 类型需求 (Type Requirements)
语法:
1 | typename type-id; |
作用:
检查某个类型是否存在。常用于检查嵌套类型(如 value_type
, iterator
)。
示例:
1 | template<typename T> |
2.2.3 复合需求 (Compound Requirements)
这是最复杂也最强大的一种,使用花括号 {}
包围表达式,并可以附加约束。
基本语法:
1 | { expression } [noexcept] [-> type-constraint]; |
这里的 ->
不是 JavaScript 的箭头函数,而是 C++ requires
表达式中用于指定返回类型约束的语法。
a: 只检查表达式合法性(等同于简单需求,但更灵活
1 | { a + b }; |
这与 a + b;
效果相同,检查 a + b
是否语法合法。
b: 检查表达式的返回类型
这才是复合需求的精髓:
1 | { a + b } -> std::same_as<T>; |
分解解释:
{ a + b }
: 被检查的表达式。->
: 引入对表达式结果的约束。std::same_as<T>
: 一个 type-constraint(类型约束),它是一个 concept,要求左侧的类型与T
完全相同。
这意味着:a + b
不仅要语法合法,其求值结果的类型还必须与 T
是同一个类型。
对比:
1 | // 只检查语法 |
c: 更多返回类型约束的例子
1 | template<typename T> |
d:检查异常规范
复合需求还可以检查表达式是否 noexcept
:
1 | { a + b } noexcept; // 要求 a + b 是 noexcept 表达式 |
2.2.4 嵌套需求 (Nested Requirements)
使用 requires
关键字在内部引入额外的逻辑条件。
1 | template<typename T> |
注意:嵌套需求中的 requires
后面直接跟一个布尔常量表达式(通常是另一个 concept 或 bool
值),而不需要分号结束(它本身就是一个声明)。
2.2.5 总结对比表
语法形式 | 示例 | 检查内容 |
---|---|---|
简单需求 | a + b; |
a + b 是否是合法表达式 |
复合需求 (无 ->) | { a + b }; |
同上,等价于简单需求 |
复合需求 (带 ->) | { a + b } -> std::same_as<T>; |
a + b 合法 且 返回类型 完全等于 T |
复合需求 (带 ->) | { a + b } -> std::convertible_to<T>; |
a + b 合法 且 返回类型 可隐式转换为 T |
复合需求 (noexcept) | { a + b } noexcept; |
a + b 合法 且 是 noexcept |
类型需求 | typename T::value_type; |
T 是否有 value_type 嵌套类型 |
嵌套需求 | requires std::integral<T>; |
额外的布尔条件 |
为什么需要 ->
形式?
因为许多操作符的返回类型是有约定的。例如:
operator+
通常返回一个新对象(值类型),而不是引用。operator==
应该返回bool
。
使用 { expr } -> constraint;
可以精确地强制这些约定,使你的 concept 更加严谨和符合直觉。而简单的 expr;
只保证了操作符的存在,无法保证其行为的正确性。
因此,当你需要对表达式的返回类型进行约束时,就必须使用复合需求的 ->
形式。
3 使用 Concepts
有多种语法形式:
形式一:函数模板参数列表中使用 (推荐)
1 | // 使用 concept 作为模板参数的约束 |
形式二:类模板约束
1 | template<Printable T> |
形式三:函数参数约束
1 | // 直接约束函数参数 |
标准库中的常用 Concepts
C++20 标准库在 <concepts>
头文件中定义了许多预定义的 Concepts,可以直接使用:
基本类型特征相关:
std::integral<T>
:T
是整型std::floating_point<T>
:T
是浮点型std::arithmetic<T>
:T
是算术类型(整型或浮点型)std::same_as<T, U>
:T
和U
是同一类型std::derived_from<Base, Derived>
:Derived
从Base
派生
对象类别相关:
std::movable<T>
:T
可移动std::copyable<T>
:T
可拷贝std::destructible<T>
:T
可析构
可调用相关:
std::invocable<F, Args...>
:F
可以用Args...
调用std::predicate<F, Args...>
:F
是一个返回布尔值的可调用对象
容器和迭代器相关:
std::default_constructible<T>
std::swappable<T>
std::ranges::input_iterator<It>
(在<ranges>
中)
优势
- 清晰的编译错误:当类型不满足概念时,错误信息直接指出哪个概念未满足,而不是深入模板实例化的细节。
- 更好的代码文档:模板的意图和要求一目了然。
- 函数重载和特化:可以根据不同的 concept 选择最合适的函数重载或类特化。
1
2
3
4
5template<std::integral T>
void foo(T) { /* 处理整型 */ }
template<std::floating_point T>
void foo(T) { /* 处理浮点型 */ } - 提高代码安全性:在编译期强制执行接口契约。
总结
C++20 的 Concepts 是模板编程的重大进步。它通过显式声明模板参数的约束条件,极大地提升了模板代码的可读性、可维护性和错误诊断能力。掌握 Concepts 是现代 C++ 编程的关键技能之一。