在 C++20 之前,模板函数的参数类型往往只能通过 SFINAE(Substitution Failure Is Not An Error)和模板特化来约束,这种方式语法繁琐、可读性差。C++20 引入了概念(Concepts),提供了一种简洁直观的方式来限定模板参数,使得模板接口更易于理解和维护。
1. 什么是概念?
概念是一种模板参数的逻辑约束,它描述了某类型必须满足的一组属性。概念本质上是一个可组合的布尔表达式,可以直接用于函数模板、类模板等的参数列表。
#include <concepts>
#include <type_traits>
template<typename T>
concept Incrementable = requires(T x) {
{ ++x } -> std::same_as<T&>;
{ x++ } -> std::same_as <T>;
};
上面定义的 Incrementable 概念要求类型 T 必须支持前置递增、后置递增,并且对应返回类型符合预期。
2. 使用概念约束函数模板
template<Incrementable T>
T sum(T first, T second) {
return first + second;
}
这段代码直接在模板参数列表中使用 Incrementable,编译器在实例化时会检查 T 是否满足该概念。如果不满足,则模板实例化失败,而不会产生 SFINAE 误解。
3. 组合多个概念
C++20 允许使用逻辑运算符组合概念,例如:
template<typename T>
concept Arithmetic = std::integral <T> || std::floating_point<T>;
template<Arithmetic T>
T multiply(T a, T b) {
return a * b;
}
也可以使用 requires 语句块来组合更复杂的约束:
template<typename T>
concept EqualityComparable = requires(T a, T b) {
{ a == b } -> std::convertible_to <bool>;
{ a != b } -> std::convertible_to <bool>;
};
template<Arithmetic T>
requires EqualityComparable <T>
T add_and_compare(T a, T b) {
T sum = a + b;
if (sum == a) return sum;
return b;
}
4. 提升可读性与错误信息
使用概念可以让错误信息更加友好。比如:
template<typename T>
concept Even = requires(T x) { x % 2 == 0; };
template<Even T>
T double_even(T x) {
return x * 2;
}
若你误传递一个奇数类型,编译器会直接指出 Even 不满足,而不是一堆隐晦的 SFINAE 失效信息。
5. 与现有代码的兼容
概念并不会改变已有模板的行为。你可以在保持 SFINAE 兼容的同时引入概念。例如:
template<typename T>
requires std::is_integral_v <T>
int add_ints(int a, int b) { return a + b; }
在不支持 C++20 的编译器上,你可以使用 enable_if 继续保持兼容。
6. 小结
- 概念是对类型的逻辑约束,提升了模板接口的可读性。
- 通过 requires 语句或 概念约束直接写在模板参数列表中,减少 SFINAE 语法复杂度。
- 组合概念和
requires能表达更复杂的约束。 - 编译器会给出更友好的错误信息,便于调试。
C++20 的概念让模板编程既安全又易懂,值得在新的项目中积极使用。