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_integralstd::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: 避免了复杂的模板元编程技巧,使泛型代码更易于编写和理解。
- 零运行时开销: 概念是纯粹的编译时特性,不会增加任何运行时性能负担。


