C++20 新增了 概念(Concepts),它是一种对模板参数进行约束的语法和机制。通过概念可以在编译期明确指出类型必须满足哪些要求,从而提升代码的可读性、可维护性,并大幅减少编译错误。下面我们从概念的基本语法、使用方式、优势以及常见应用场景四个方面进行系统阐述,并给出完整代码示例。
1. 概念的核心思想
传统 C++ 模板在调用时会对类型进行“隐式”推断,如果实参不满足函数或类模板所期望的特性,编译器会在错误的地方给出“模板参数不匹配”的信息。概念则将这些约束显式声明在模板参数列表之前,让编译器在匹配阶段就能检查是否满足,错误信息更精准、提示更友好。
例:在
std::sort中,要求第一个迭代器与第二个迭代器是 可随机访问 的;要求第三个迭代器与前两个是 可比较 的。概念把这些要求写成std::random_access_iterator、std::sortable等,使得std::sort的签名更易读。
2. 语法与定义
2.1 基本语法
// 语法格式
concept ConceptName = expression;
// 典型的概念定义
concept Integral = std::is_integral_v <T>;
2.2 使用在模板参数中
template<Integral T>
void foo(T value) { /* ... */ }
template<std::ranges::input_range R>
void bar(R&& r) { /* ... */ }
2.3 约束表达式(Constraint Expression)
约束表达式是一个布尔表达式,使用 requires 关键字:
template<typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::convertible_to <T>;
};
上述定义表示 T 必须支持 operator+ 并且结果可转换为 T。
3. 优势与对比
| 传统模板 | 使用概念后 |
|---|---|
| 编译错误信息混乱,往往在调用点出现 | 编译错误指向概念定义处,信息清晰 |
| 无法在参数列表中写入“类型必须满足 X 条件” | 直接在签名中描述约束 |
| 可能导致模板实例化过度(SFINAE 失效) | 编译器在匹配阶段就能过滤不合格类型 |
| 难以维护,尤其是大型模板库 | 约束集中,易于阅读与复用 |
4. 常见标准库概念
| 概念 | 描述 |
|---|---|
std::same_as<T, U> |
T 与 U 必须是相同类型 |
std::derived_from<T, U> |
T 必须继承自 U |
std::constructible_from<T, Args...> |
T 能被 Args… 构造 |
| `std::ranges::input_range | |
| ` | R 必须是输入范围 |
std::sortable<Iter, Comp> |
迭代器 Iter 必须支持 Comp 比较 |
5. 实战案例
5.1 用概念实现一个泛型 min 函数
#include <concepts>
#include <utility>
template<typename T>
concept LessThanComparable = requires(T a, T b) {
{ a < b } -> std::convertible_to<bool>;
};
template<LessThanComparable T>
constexpr T my_min(const T& a, const T& b) noexcept {
return (b < a) ? b : a;
}
int main() {
int i1 = 10, i2 = 20;
std::cout << my_min(i1, i2) << '\n'; // 10
std::string s1 = "apple", s2 = "banana";
std::cout << my_min(s1, s2) << '\n'; // apple
}
编译器会在 my_min 的模板参数处检查 LessThanComparable,如果传入的类型不支持 <,就会给出明确的错误提示。
5.2 用概念实现一个通用的 swap 函数
#include <concepts>
#include <utility>
template<typename T>
concept Swappable = requires(T& a, T& b) {
{ std::swap(a, b) } -> std::same_as <void>;
};
template<Swappable T>
constexpr void universal_swap(T& a, T& b) noexcept {
std::swap(a, b);
}
6. 进阶:自定义概念与递归约束
有时我们需要组合已有概念来构造更细粒度的约束。可以使用 &&、|| 以及 requires 子句。
template<typename T>
concept IntegralOrEnum = std::integral <T> || std::enum<T>;
template<IntegralOrEnum T>
void handle_int_or_enum(T val) {
// ...
}
7. 小结
- 概念 是对模板参数约束的显式声明,提升编译器错误信息质量。
- 语法简洁:
concept Name = expression;或requires表达式。 - 标准库已提供大量概念,能直接用于自定义函数/类。
- 通过概念可实现更安全、更易维护的泛型代码,尤其适合大型项目和模板库开发。
实战建议:在编写任何需要泛型支持的代码时,先为其定义适当的概念;将模板参数约束化后,再用
requires或concept约束调用方。这样能让代码更易读、错误定位更快,也为未来的代码重构打下坚实基础。