C++20 Concepts:类型约束革命
C++20 Concepts:类型约束革命
模板是 C++ 泛型编程的基石,但 C++98 到 C++17 的模板一直有一个核心痛点:对模板参数的要求隐藏在函数体内,只有当用户传入不正确的类型时,才在实例化阶段爆出数页深不可测的错误信息。 SFINAE、enable_if、static_assert 都是在这条链路上打补丁。
C++20 Concepts 从根本上改变了这一点。它让"模板参数必须满足什么条件"成为了接口的显式部分。
1. 问题:为什么 SFINAE 不够?
template<typename T>
auto add(T const& a, T const& b) {
return a + b; // 如果 T 不支持 +,这里爆一堆模板实例化回溯
}SFINAE 时代的标准答案:
template<typename T>
auto add(T const& a, T const& b)
-> std::enable_if_t<std::is_arithmetic_v<T>, T> {
return a + b;
}这段代码的问题不是它不工作,而是:
- 错误信息仍然糟糕:类型不满足
enable_if条件时,不是"T 必须支持 +",而是"没有匹配的add重载"。 - 约束和实现分离:约束在返回类型上,实现在函数体里,两处代码物理距离远。
- 不可组合:写一个"可加的"(Addable)概念,在 SFINAE 下需要用
enable_if和void_t的组合技。
2. concept:定义约束的第一公民
C++20 的 concept 把约束提升为一等公民:
template<typename T>
concept Addable = requires(T a, T b) {
a + b; // 检查 a + b 是否合法
{ a + b } -> std::same_as<T>; // 检查返回值类型
};使用时有两种语法:
// 语法 1:requires 子句
template<typename T>
requires Addable<T>
auto add(T const& a, T const& b) { return a + b; }
// 语法 2:概念名代替 typename
template<Addable T>
auto add(T const& a, T const& b) { return a + b; }
// 语法 3:缩写的函数模板(C++20 新语法)
auto add(Addable auto const& a, Addable auto const& b) { return a + b; }语法 2 最接近人类阅读习惯:"add 接受一个满足 Addable 概念的类型 T"。
3. requires 的四种形式
requires 是 Concepts 的核心语法,它出现在四个不同位置,含义各有侧重:
3.1 requires 子句(requires clause)
template<typename T>
requires std::integral<T> // ← requires 子句,跟在模板参数列表后面
T gcd(T a, T b) { return b == 0 ? a : gcd(b, a % b); }3.2 requires 表达式(requires expression)
template<typename T>
concept Hashable = requires(T v) {
{ std::hash<T>{}(v) } -> std::convertible_to<std::size_t>;
// 检查:std::hash<T> 能否默认构造 + 调用后返回可转为 size_t 的类型
};requires 表达式有四种检查形式:
requires(T a, T b) {
a + b; // 简单要求:这个表达式必须合法
typename T::value_type; // 类型要求:T 必须有嵌套类型 value_type
{ a + b } -> std::same_as<T>; // 复合要求:表达式合法 + 返回类型匹配
requires sizeof(T) > 4; // 嵌套要求:编译期 bool 表达式
};3.3 requires 约束非模板成员函数
template<typename T>
class Vector {
public:
T& operator[](size_t i) { return data_[i]; }
// 只有当 T 支持 == 时才启用这个成员函数
bool operator==(Vector const& other) const
requires std::equality_comparable<T>
{
return std::equal(data_, data_ + size_, other.data_);
}
};3.4 requires 后跟常量表达式
static_assert(requires { T::value; }, "T must have ::value");在 static_assert 中,requires 表达式本身是一个编译期 bool,可以用在任何需要 bool 的地方。
4. 标准库概念的层次结构
C++20 标准库(<concepts>)提供了一套精心设计的概念层次:
std::integral ← 整数类型
std::floating_point ← 浮点类型
std::signed_integral ← 有符号整数
std::unsigned_integral ← 无符号整数
std::equality_comparable ← 支持 == / !=
std::totally_ordered ← 支持 < > <= >=
└─ equality_comparable
std::movable ← 可移动构造 + 可移动赋值
std::copyable ← 可拷贝 + 可移动
std::semiregular ← 可拷贝 + 可默认构造
std::regular ← semiregular + equality_comparable
└─ totally_ordered
std::invocable<F, Args...> ← F 可以用 Args 调用
std::predicate<F, Args...> ← invocable + 返回 bool
std::relation<F, T, U> ← predicate + 满足某种关系这组层次让"表达类型期望"有了标准化的词汇:
template<std::regular T>
class container { /* ... */ }; // T 必须行为正常(可拷贝、可比较等)5. Concepts vs SFINAE:工程对比
| 维度 | SFINAE (C++17) | Concepts (C++20) |
|---|---|---|
| 错误信息 | 多页模板回溯 | 一行"约束不满足" + 明确的失败原因 |
| 重载决议 | 通过 enable_if 的存在性切换 | 概念之间有 partial ordering,自动选择最特化的 |
| 阅读性 | 约束散落在返回类型/默认参数中 | 写在模板参数位置,一眼可见 |
| IDE 友好 | 需要实例化才能检查 | 在模板定义阶段即可检测约束违背 |
| 组合性 | 需要写复杂的 conjunction / void_t | 用 && || 直接组合概念 |
6. 概念子集与重载决议
Concepts 支持偏序(partial ordering),编译器会自动选择约束更强的重载:
template<typename T>
concept RandomAccess = std::random_access_iterator<T>;
template<typename T>
concept Bidirectional = std::bidirectional_iterator<T> && !RandomAccess<T>;
template<Bidirectional Iter>
void advance(Iter& it, ptrdiff_t n) { /* 一步一步移动 */ }
template<RandomAccess Iter>
void advance(Iter& it, ptrdiff_t n) { it += n; /* 直接跳 */ }编译器根据哪个概念约束更强来选择重载——不需要手写 if constexpr 或 tag dispatch。
7. 编写自定义概念的最佳实践
不要"过约束"
// 坏:要求了不需要的 <
template<typename T>
concept Sortable = std::totally_ordered<T> && std::random_access_iterator<T>;
// 好:排序只需要 <,不要把 > == != 都扯进来
template<typename T>
concept Sortable = requires(T a, T b) { a < b; };使用语义概念而非纯语法概念
// 语法概念(弱)
template<typename T>
concept HasPlus = requires(T a, T b) { a + b; };
// 语义概念(强):告诉调用者 + 的实际意义是什么
template<typename T>
concept Addable = std::is_arithmetic_v<T>;
// 或者直接使用标准库的 concept(如果存在)纯粹依赖 requires 的表达式检查可以描述语法,但不能描述语义。一个满足 HasPlus 的类型可能把 + 实现为字符串连接,而你的算法期待的是"加法"。
总结
Concepts 不是 SFINAE 的语法糖,而是把"类型约束"从"在函数体内隐性表达"提升为"在接口上显式声明"。它带来的三重收益——清晰的错误信息、精确的重载决议、模板定义阶段的约束检查——让 C++ 的泛型编程终于有了类型安全的"契约"。