在 C++20 之前,模板的参数约束往往需要使用 enable_if、SFINAE 或者 requires 关键字来实现。虽然这些技术功能强大,但代码可读性差、错误信息难以理解。C++20 引入了 概念(Concepts),为模板编程提供了更加直观和类型安全的约束方式。本文将从基本语法、常见概念以及实践应用三个方面,介绍如何利用概念来简化模板约束。
1. 概念的基本语法
概念本质上是一个布尔表达式,用来描述一个类型或一组类型所满足的属性。基本定义方式如下:
template<typename T>
concept MyConcept = requires(T a, T b) {
// 需要满足的表达式
{ a + b } -> std::same_as <T>; // 表达式必须可用,且返回类型为 T
{ a == b } -> std::convertible_to <bool>;
};
requires关键字后跟一个表达式列表,表达式使用->指定返回类型约束。requires也可以直接使用类型或值来检查是否可调用,例如requires (T{1,2} + T{3,4});。
概念定义后可以在函数、类、变量等模板中使用:
template<MyConcept T>
T add(T a, T b) {
return a + b;
}
如果传入的类型不满足 MyConcept,编译器会在错误信息中指明哪个约束不满足,从而提高可调试性。
2. 常见标准概念
C++20 标准库提供了大量预定义概念,常用的有:
| 概念 | 说明 | 头文件 |
|---|---|---|
std::integral |
整数类型 | ` |
| ` | ||
std::floating_point |
浮点数类型 | ` |
| ` | ||
std::arithmetic |
整数或浮点数 | ` |
| ` | ||
| `std::same_as | ||
| 与类型 T 相同 |` |
||
| `std::derived_from | ||
| 从 Base 派生 |` |
||
std::constructible_from<T...> |
可以用给定参数列表构造 | <concepts> |
std::movable |
可移动类型 | ` |
| ` |
使用这些概念可以极大简化代码。例如,想要实现一个通用的 swap:
template<std::movable T>
void my_swap(T& a, T& b) {
T temp = std::move(a);
a = std::move(b);
b = std::move(temp);
}
3. 组合概念:多约束
有时一个模板需要满足多个约束。可以使用逻辑运算符 &&、|| 和 ! 组合概念:
template<typename T>
concept IntegralOrPointer = std::integral <T> || std::is_pointer_v<T>;
也可以自定义组合概念:
template<typename T>
concept Numeric = std::integral <T> || std::floating_point<T>;
template<Numeric T>
T multiply(T a, T b) {
return a * b;
}
4. 自定义概念示例:可排序的容器
假设我们想实现一个泛型排序函数,只接受可随机访问、支持比较的容器。可以定义如下概念:
#include <concepts>
#include <iterator>
template<typename Container>
concept RandomAccessSortable = requires(Container c) {
{ std::begin(c) } -> std::input_iterator;
{ std::end(c) } -> std::input_iterator;
{ std::is_sorted(std::begin(c), std::end(c)) } -> std::convertible_to <bool>;
};
template<RandomAccessSortable C>
void my_sort(C& container) {
std::sort(std::begin(container), std::end(container));
}
5. 概念与 SFINAE 的对比
- SFINAE:基于模板特化的错误隐藏,错误信息难以定位,适用于旧标准或编译器不支持 C++20 的场景。
- 概念:直接在函数签名中声明约束,错误信息更清晰,编译速度更快。
在现代 C++20 代码中,建议优先使用概念。
6. 进阶话题:概念与 requires 子句
在函数模板内部也可以使用 requires 子句进一步限制模板参数:
template<typename T>
auto min(const T& a, const T& b)
requires std::totally_ordered <T> // 需要满足可比较
{
return (a < b) ? a : b;
}
与概念一起使用,能够实现更细粒度的约束。
7. 小结
- 概念 让模板约束变得更具可读性、可维护性和可调试性。
- 标准概念 已经涵盖大多数常见需求,配合
requires子句即可快速构建安全的泛型代码。 - 自定义概念 可以根据项目需求抽象出更高层次的抽象,提升代码复用率。
掌握概念后,你的 C++ 模板代码将不再依赖繁琐的 SFINAE 伪技巧,而是能够以更自然、更类型安全的方式进行约束。祝你编码愉快!