concepts
在 C++20 中,”约束” (Constraints) 主要指的是 **概念 (Concepts)**。它是 C++ 模板编程中的一个革命性特性,旨在解决传统模板编程中的一些核心痛点。
简单来说,概念 (Concept) 允许你指定模板参数必须满足的条件或要求。
1. 为什么需要概念 (Concepts)?
在 C++20 之前,我们编写泛型代码(模板)时,编译器通常只在实例化模板时才检查模板参数是否“合适”。如果参数不合适,就会导致非常冗长、难以理解的编译错误信息,这被称为 SFINAE (Substitution Failure Is Not An Error) 的副作用。
考虑一个简单的例子:
1 | // C++11/14/17 |
当 print_sum
被 std::vector<int>
实例化时,a + b
操作会失败,导致编译器抛出一大堆错误,告诉你 operator+
不适用于 std::vector
。这些错误信息往往是模板内部的实现细节,而不是清晰地指出“你传入的类型不支持加法操作”。
概念就是为了解决这些问题而生:
- 改善错误信息: 编译器可以在实例化之前检查模板参数是否满足概念要求,并给出清晰、友好的错误信息。
- 提高可读性: 模板的接口变得自文档化,开发者可以一眼看出模板参数需要具备哪些能力。
- 简化模板元编程: 替代了复杂的 SFINAE 技术,使条件编译更加直观。
- 更好的重载解析: 概念可以参与函数模板的重载解析,帮助编译器选择最合适的模板。
2. 概念 (Concepts) 是什么?
一个概念定义了一组编译时可检查的约束,这些约束描述了模板参数必须支持的操作或特性。
核心思想: 不再关注类型本身是什么,而是关注类型能做什么。
3. 如何定义和使用概念?
C++20 引入了 concept
关键字和 requires
关键字(requires
表达式和 requires
子句)。
3.1 定义一个概念
使用 concept
关键字定义一个概念。概念的定义体是一个 requires
表达式,其中列出了对模板参数的各种要求。
1 |
|
concept Addable = ...
:声明一个名为Addable
的概念。requires(T a, T b)
:这是一个requires
表达式,它定义了对类型T
的要求。括号内的T a, T b
声明了用于测试的变量,这些变量不会实际存在于运行时,仅用于编译时检查。{ a + b } -> std::same_as<T>;
:这是一个**复合要求 (Compound Requirement)**。{ a + b }
:要求表达式a + b
是有效的。-> std::same_as<T>
:要求表达式a + b
的结果类型与T
相同。
{ std::cout << (a + b) } -> std::same_as<std::ostream&>;
:要求a + b
的结果可以被std::cout
操作符<<
输出,并且返回类型是std::ostream&
。
3.2 使用概念约束模板参数
有几种方式可以使用概念来约束模板参数:
a) 作为模板参数的类型限制 (最常见和推荐)
1 | // 使用 Addable 概念约束模板参数 T |
b) 使用 requires
子句 (Trailing requires
Clause)
当概念定义比较简单,或者不想单独定义一个概念时,可以直接在模板声明后使用 requires
子句。
1 | template <typename T> |
c) 使用 requires
表达式作为 if constexpr
的条件
虽然概念主要用于模板参数约束,但 requires
表达式也可以在 if constexpr
中用于编译时条件判断。
1 | template <typename T> |
3.3 标准库概念
C++20 标准库也提供了许多预定义的概念,例如:
std::integral
:要求类型是整数类型。std::floating_point
:要求类型是浮点类型。std::signed_integral
,std::unsigned_integral
std::same_as<T, U>
:要求T
和U
是相同的类型。std::totally_ordered<T>
:要求类型T
支持所有比较运算符 (==
,!=
,<
,>
,<=
,>=
)。std::invocable<F, Args...>
:要求函数对象F
可以用Args...
参数调用。std::ranges::range
:要求类型是一个范围 (Ranges)。
你可以直接使用这些概念来约束你的模板:
1 |
|
4. 总结概念 (Concepts) 的优势
- 清晰的接口定义: 模板的意图一目了然,提高了代码的可读性和可维护性。
- 友好的编译错误: 编译器能给出准确、易懂的错误信息,大大缩短了调试时间。
- 编译时检查: 在编译阶段就能发现类型不匹配的问题,而不是等到运行时。
- 更好的重载解析: 概念可以帮助编译器在多个模板重载中选择最匹配的一个。
- 替代 SFINAE: 避免了复杂的模板元编程技巧,使泛型代码更易于编写和理解。
- 零运行时开销: 概念是纯粹的编译时特性,不会增加任何运行时性能负担。