C++20 Concepts通过引入concept关键字和requires表达式,为模板参数提供清晰的编译期约束,取代了晦涩的SFINAE机制,使代码意图更明确、错误信息更友好,显著提升了模板代码的可读性与可维护性。

C++模板约束概念,也就是我们常说的C++20 Concepts,本质上是给模板参数加了一层“契约”或“类型要求”。它允许你在编译期明确地指定模板参数需要满足的条件,比如它必须支持某个操作、拥有某个成员类型,或者满足某个特定的概念。这套机制的核心在于
requires
解决方案
在C++20之前,我们为了约束模板参数,通常会依赖SFINAE(Substitution Failure Is Not An Error)机制,比如
std::enable_if
std::void_t
要理解C++模板约束,我们首先要掌握
concept
requires
concept
立即学习“C++免费学习笔记(深入)”;
template <typename T>
concept Addable = requires(T a, T b) {
{ a + b } -> std::same_as<T>; // 要求a+b是一个合法的表达式,并且结果类型与T相同
};
template <typename T>
concept Printable = requires(T t, std::ostream& os) {
{ os << t } -> std::same_as<std::ostream&>; // 要求T可被输出到流
};这里,
requires(...) { ... }requires
requires(T t) {
t.foo(); // 要求T有一个名为foo的成员函数
*t; // 要求T可被解引用
};requires(T t) {
typename T::value_type; // 要求T有一个名为value_type的嵌套类型
};requires(T t) {
{ t.get() } -> int; // 要求t.get()合法,且结果类型可转换为int
{ t.data() } -> std::same_as<int&>; // 要求t.data()合法,且结果类型必须精确匹配int&
{ t.reset() } noexcept; // 要求t.reset()合法,且必须是noexcept的
};requires
concept
template <typename T>
concept MyComplexConcept = requires(T t) {
requires Addable<T>; // 要求T满足Addable概念
requires Printable<T>; // 要求T满足Printable概念
};定义好
concept
template <Addable T> // 在模板参数列表直接使用concept
T add(T a, T b) {
return a + b;
}
void print_something(Printable auto p) { // C++20的简写模板语法,用concept约束auto
std::cout << p << std::endl;
}
template <typename T>
requires Addable<T> && Printable<T> // 在requires子句中使用concept组合
void process(T val) {
std::cout << add(val, val) << std::endl;
}这种显式的约束,让代码的意图一目了然。当传入不符合要求的类型时,编译器会给出清晰的错误信息,直接告诉你哪个概念的哪个要求没有被满足,而不是一堆SFINAE失败的内部实现细节。
说实话,这是Concepts最让我感到兴奋的地方。过去,写模板代码就像是在走钢丝,你不知道什么时候会因为某个类型不支持某个操作而导致编译失败,而且那错误信息,简直就是天书。比如,你试图对一个没有
operator+
Concepts的引入,彻底改变了这种局面。它通过强制模板参数在编译时满足预定义的语义契约,将潜在的错误从运行时提前到了编译时,并且以一种极其友好的方式呈现出来。
考虑一个简单的例子:
// 传统方式,如果T不支持+,这里会SFINAE失败,错误信息可能很长
template <typename T>
auto sum_old(T a, T b) {
return a + b;
}
// 使用Concepts
template <typename T>
concept HasPlusOperator = requires(T a, T b) {
{ a + b };
};
template <HasPlusOperator T>
T sum_new(T a, T b) {
return a + b;
}
struct NoPlus {};
int main() {
// sum_old(NoPlus{}, NoPlus{}); // 编译错误,错误信息可能很复杂
sum_new(NoPlus{}, NoPlus{}); // 编译错误,错误信息清晰:'NoPlus' does not satisfy 'HasPlusOperator'
// the expression 'a + b' is not satisfied
}你看,使用
sum_new
NoPlus
HasPlusOperator
a + b
这不仅仅是让错误信息好看那么简单,它还提升了代码的可读性。当我在阅读一个模板函数签名时,如果看到
template <Printable T>
T
std::enable_if
requires
requires
基础结构: 一个
requires
requires
requires (parameters) {
// requirement-list
}parameters
requires
requires
四种主要要求类型:
简单要求 (Simple Requirements): 这可能是最常见和最直观的要求。它只是简单地检查一个表达式是否是合法的。如果表达式合法,则该要求满足。
requires(T t) {
t.size(); // 要求T有一个名为size()的成员函数
++t; // 要求T支持前缀递增操作
};这类要求不关心表达式的结果类型或其noexcept属性,只关心它是否能编译通过。
类型要求 (Type Requirements): 当我们需要检查一个类型是否具有特定的嵌套类型或别名时,就会用到它。
requires(T t) {
typename T::value_type; // 要求T内部定义了名为value_type的类型
typename T::iterator; // 要求T内部定义了名为iterator的类型
};这里
typename
T::value_type
复合要求 (Compound Requirements): 这是最强大的要求形式,它不仅检查表达式的合法性,还可以进一步约束其结果类型和noexcept属性。 语法是
{ expression } -> ReturnTypeConstraint;{ expression } noexcept;{ expression } -> ReturnTypeConstraint noexcept;-> ReturnTypeConstraint
ReturnTypeConstraint
{ a + b } -> int;std::same_as<U>
U
{ a + b } -> std::same_as<T>;std::convertible_to<U>
U
{ a + b } -> std::convertible_to<T>;-> T;
noexcept
{ expr } noexcept;expr
noexcept
{ expr } -> ReturnTypeConstraint noexcept;requires(T t) {
{ t.get_value() } -> std::same_as<int>; // 要求t.get_value()返回int
{ t.write_data() } noexcept; // 要求t.write_data()是noexcept的
{ t.calculate() } -> double noexcept; // 要求t.calculate()返回double且是noexcept的
};嵌套要求 (Nested Requirements): 允许你在一个
requires
requires
concept
requires(T t) {
requires std::integral<T>; // 要求T满足标准库的integral概念
requires requires(T other) { // 嵌套的requires表达式
{ t == other } -> bool;
};
};这种嵌套可以用来构建更复杂的约束逻辑,或者将多个概念组合起来。
组合与逻辑操作: 多个要求可以通过
&&
||
template <typename T>
concept Arithmetic = requires(T a, T b) {
{ a + b } -> std::same_as<T>;
{ a - b } -> std::same_as<T>;
{ a * b } -> std::same_as<T>;
{ a / b } -> std::same_as<T>;
};
template <typename T>
concept Numeric = std::integral<T> || std::floating_point<T>; // 使用逻辑或组合标准库概念通过这些语法元素,
requires
在我看来,C++20 Concepts的出现,基本宣告了传统SFINAE(Substitution Failure Is Not An Error)在模板约束领域的“退休”。虽然SFINAE在C++的历史中扮演了至关重要的角色,解决了许多类型检查和重载决议的问题,但它的复杂性和晦涩性也让无数C++开发者头疼不已。
何时选择Concepts?
答案很简单:几乎总是选择Concepts。
std::enable_if
std::void_t
何时SFINAE可能仍然出现?
虽然我极力推荐Concepts,但在某些特定场景下,你可能仍然会遇到或需要使用SFINAE:
如何从SFINAE迁移到Concepts?
迁移过程通常涉及将SFINAE的条件逻辑转化为
concept
requires
std::enable_if
SFINAE:
template <typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
void func(T t) { /* ... */ }Concepts:
template <std::integral T> // 直接使用标准库概念
void func(T t) { /* ... */ }
// 或者自定义概念
template <typename T>
concept MyIntegral = std::is_integral_v<T>;
template <MyIntegral T>
void func(T t) { /* ... */ }std::enable_if
SFINAE (通常结合std::void_t
template <typename T, typename = std::void_t<decltype(std::declval<T>().foo())>>
void func(T t) { /* ... */ }Concepts:
template <typename T>
concept HasFoo = requires(T t) {
t.foo(); // 简单要求即可
};
template <HasFoo T>
void func(T t) { /* ... */ }std::enable_if
SFINAE:
template <typename T, typename = std::enable_if_t<std::is_same_v<decltype(std::declval<T>().get()), int>>>
void func(T t) { /* ... */ }Concepts:
template <typename T>
concept HasIntGetter = requires(T t) {
{ t.get() } -> std::same_as<int>; // 复合要求,精确匹配返回值
};
template <HasIntGetter T>
void func(T t) { /* ... */ }迁移的关键在于识别SFINAE条件中试图表达的“类型要求”,然后用
requires
以上就是C++模板约束概念 类型要求表达式语法的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号