在C++20之前,模板编程往往伴随着大量的SFINAE(Substitution Failure Is Not An Error)技巧,用来限制模板参数的类型范围。这样不仅代码难以阅读,还容易产生误导性的错误信息。C++20引入了概念(Concepts),为模板参数提供了一种更直观、可读性更高的约束方式。本文将从概念的定义、实现细节,到在实际项目中的应用示例,帮助你快速上手。
1. 概念的基本语法
// 定义一个名为 Incrementable 的概念
template <typename T>
concept Incrementable = requires(T x) {
{ ++x } -> std::same_as<T&>;
{ x++ } -> std::same_as <T>;
};
概念本质上是一种谓词,它接受一组类型或值,并在编译时进行布尔判定。使用 requires 关键字可以写出更自然的语法,描述类型必须满足的表达式和返回值。
2. 组合与层级概念
概念可以像布尔表达式一样组合:
template <typename T>
concept Arithmetic = Incrementable <T> && std::is_arithmetic_v<T>;
template <typename T>
concept Integral = std::is_integral_v <T>;
template <typename T>
concept SignedIntegral = Integral <T> && std::is_signed_v<T>;
层级化的概念可以帮助我们构建更复杂的约束体系,而不会使每个函数显得冗长。
3. 用概念替代SFINAE
3.1 传统SFINAE写法
template <typename T,
std::enable_if_t<std::is_integral_v<T>, int> = 0>
T add(T a, T b) { return a + b; }
3.2 使用概念
template <typename T>
requires Arithmetic <T>
T add(T a, T b) { return a + b; }
或者更简洁的模板参数语法:
template <Arithmetic T>
T add(T a, T b) { return a + b; }
这样既省去了 enable_if 的冗余代码,也让错误信息更易读。
4. 概念与函数重载优先级
在使用概念进行约束时,编译器会根据约束的匹配程度决定重载选择。以下示例展示了概念对重载优先级的影响:
void process(std::string_view sv) { std::cout << "string_view\n"; }
template <std::integral T>
void process(T value) { std::cout << "integral\n"; }
int main() {
process("hello"); // 选择 string_view
process(42); // 选择 integral
}
如果在两者之间存在重叠,概念会使匹配更精确,从而避免模糊匹配。
5. 实战:自定义泛型容器的约束
假设你要实现一个泛型堆栈 `Stack
`,只允许具备默认构造、复制、移动和可比较的类型。 “`cpp template concept Storable = std::default_initializable && std::copyable && std::movable && std::three_way_comparable ; template class Stack { public: void push(const T& value) { data.push_back(value); } T pop() { if (data.empty()) throw std::out_of_range(“Stack empty”); T value = std::move(data.back()); data.pop_back(); return value; } private: std::vector data; }; “` 此处,`Storable` 组合了多种标准概念,确保所有用来实例化 `Stack` 的类型都满足堆栈所需的基本操作。若尝试使用不满足约束的类型,编译器会给出清晰的错误信息。 ### 6. 概念与 constexpr C++20 的概念可以与 `constexpr` 配合使用,实现在编译期进行更严格的类型检查。例如: “`cpp template constexpr T factorial(T n) { if (n