C++20的概念与约束

🧠 导读:这篇文章怎么读?

  • 1|背景与动机:先看 C++20 为啥要引入 Concepts,它解决了模板错误难读、SFINAE 写法复杂等问题。

  • 2|基础定义:再看 concept 的基本语法和几种典型定义方式(类型萃取、常量表达式、简单 requires)。

  • 3|requires 四种要求:搞清楚简单要求、类型要求、复合要求、嵌套要求分别长什么样,各自检查什么。

  • 4|使用姿势:对比几种用法:写在模板参数里、前置/尾置 requires、临时约束、约束 auto

  • 5|约束组合:理解如何用 && / || / ! 和参数包折叠把多个约束拼成一个更强的 Concept。

  • 6|偏序规则:看“更受约束的模板优先被选中”的几个重载示例,理解编译器选哪个模板。

  • 7|实战案例:重点看三个例子:HashType 限制 hashmap key、PrintableRange 限制可打印容器、IteratorType 修正 vector 构造歧义。

  • 8|Concepts vs SFINAE:对比同一个需求在 C++17 和 C++20 下的写法,感受可读性的差异。

  • 9|Checklist 总结:最后用 checklist 回顾:写 Concept 前后应该思考什么、怎么优先用标准库概念,以及如何给常用模板加语义标签。

建议:先按上面这 1~9 点快速扫一遍章节标题,心里有个结构,再根据自己当前项目/作业的需求重点看第 4、7、8 部分。


🧩 C++20 概念与约束:让模板真正“说人话”

C++20 概念(Concepts)是这一代标准里最改变模板体验的特性之一。它把原来“靠注释说明”的模板约束,变成了真正的语言级规则,让编译器帮你检查、帮你报更清晰的错。

💡 可以这样理解:Concept = “给模板参数加标签和规则”,谁不符合规则,就在模板实例化之前被拒之门外。


📌 1. 概念是什么?解决了什么问题?

在 C++20 之前:

  • 模板参数只有 typename Tclass T,几乎没有语义信息

  • 约束主要靠 **SFINAE / enable_if / decltype** 等技巧

  • 一旦写错类型,编译器报一大堆“读不懂”的模板错误

在 C++20 之后:

  • concept 定义编译期谓词(返回 bool 的条件)

  • 使用概念直接约束模板参数,不满足就不匹配模板

  • 错误信息会以“哪个 concept 不满足”的形式出现,可读性大幅提升

简单一句话:

Concept = 编译期布尔表达式 + 模板参数语义标签


🧱 2. 如何定义一个概念?(基础语法)

标准形式:

1
2
3
4
5
// template-parameter-list 是模板参数列表
// concept-name 是概念名
// constraint-expression 是可求值为 bool 的常量表达式
template <template-parameter-list>
concept concept-name = constraint-expression;

课件里的几种典型写法:

1
2
3
4
#include <type_traits>

template<typename T>
concept Integral = std::is_integral_v<T>;

含义:T 必须是整型int/long/char 等)。

1
2
template<typename T>
concept SmallType = sizeof(T) <= 4;

含义:T 的大小 ≤ 4 字节

1
2
3
4
5
template<typename T>
concept Incrementable = requires(T t) {
++t; // 检查前缀++是否有效
t++; // 检查后缀++是否有效
};

含义:T 必须支持前后缀自增


🔍 3. requires 表达式的四种形式

课件给了一个框架:

1
2
3
4
template<typename T>
concept ConceptName = requires (/* parameter-list 可选 */) {
/* requirement-seq */
};

requires \\\{ ... \\\} 里面,可以出现四类“要求”:

只检查“这条表达式能不能编译过”。

1
2
3
4
5
6
#include <concepts>

template<typename T>
concept Addable = requires(T a, T b) {
a + b; // 只要 a + b 这句能编译就行
};

检查某个嵌套类型是否存在:

1
2
3
4
5
template<typename T>
concept HasValueType = requires {
typename T::iterator; // 必须有 iterator
typename T::value_type; // 必须有 value_type
};

同时约束“表达式能否编译 + 返回类型 + noexcept 与否”。

语法:

1
{ 表达式 } [noexcept] -> 类型约束;

示例:

1
2
3
4
5
6
7
#include <concepts>

template<typename T>
concept ConvertibleAddable = requires(T a, T b) {
{ a + b } -> std::convertible_to<T>; // a+b 可转成 T
{ a += b } noexcept -> std::same_as<T&>; // a+=b 不抛异常且返回 T&
};

requires 里再写 requires,放更复杂的逻辑条件:

1
2
3
4
5
6
7
#include <type_traits>

template<typename T>
concept ComplexConcept = requires(T t) {
requires sizeof(T) <= sizeof(long); // 额外静态布尔条件
requires std::is_class_v<T>; // 必须是类类型
};

🧪 4. 概念在模板里的几种用法

课件总结了 5 种常见用法:

1
2
3
4
5
6
7
8
9
10
11
#include <concepts>
#include <type_traits>

template<class T>
concept Integral = std::is_integral_v<T>;

// 1)模板参数处直接使用 Concept
template<Integral T>
void f1(T x) {
std::cout << "有 concepts 约束" << std::endl;
}

调用:

1
2
f1(1);      // OK
f1("xxx"); // 编译期报错:不满足 Integral
1
2
3
template<typename T>
requires Integral<T>
void f2(T x) {}
1
2
template<typename T>
void f3(T x) requires Integral<T> {}

适用于“只在这个函数体里临时需要的条件”:

1
2
3
template<typename T>
requires requires(T x) { x.size(); x + x; }
void f4(T x) {}

含义:T 必须有 **size()** 成员,并且 **x + x** 可行。

1
void f5(Integral auto x) {}

比起泛写 auto,这里要求参数必须是整型。


🔗 5. 约束表达式的逻辑组合

概念本质上是“返回 bool 的约束表达式”,自然可以用逻辑运算:

  • 单个概念:std::integral<T>

  • 单个 requires 表达式

  • 单个 constexpr bool 表达式

所有约束都为真,整体才为真。

1
2
3
4
5
6
7
8
9
10
11
#include <concepts>
#include <iostream>

using std::cout;

// 必须既是整数,又能输出

template<typename T>
concept IntegralAndPrintable = std::integral<T> && requires(T t) {
cout << t;
};

至少一个约束为真,整体就为真。

1
2
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
1
2
template<typename T>
concept NonPointer = !std::is_pointer_v<T>;

对参数包做“约束折叠”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <type_traits>
#include <iostream>

using std::cout;
using std::endl;

template <class T> concept A = std::is_move_constructible_v<T>;
template <class T> concept B = std::is_copy_constructible_v<T>;
template <class T> concept C = A<T> && B<T>;

template <class... T>
requires (C<T> && ...)
void g(T...) {
cout << "g()->C" << endl;
}

这里 (C<T> && ...) 就是对参数包做逻辑与折叠。


⚖️ 6. 约束的偏序规则:重载如何选择“更合适”的模板?

当多个模板都“能匹配”实参时,编译器需要决定选哪个,这里就用到了约束偏序(constraint ordering)

如果约束 A 蕴含约束 B(A ∈ B),那么 A 比 B “更受约束”,编译器会优先选 A。

课件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
#include <concepts>
#include <type_traits>

// 要求 T 是整型

template<class T>
concept Integral = std::is_integral_v<T>;

// 有 concept 约束的版本
template<Integral T>
void f(T x) {
std::cout << "有 concepts 约束" << std::endl;
}

// 无约束的版本
template<class T>
void f(T x) {
std::cout << "无 concepts 约束" << std::endl;
}

int main() {
f(1); // 同时匹配两个模板,但 Integral 版本更受约束,被优先选择
f("xxx"); // 只匹配无约束版本
}

再看更复杂一点的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <class T> concept A = std::is_move_constructible_v<T>;
template <class T> concept B = std::is_copy_constructible_v<T>;
template <class T> concept C = A<T> && B<T>;

template <class T>
requires A<T>
void g(T x) {
std::cout << "g()->A" << std::endl;
}

template <class T>
requires C<T>
void g(T x) {
std::cout << "g()->C" << std::endl;
}

int main() {
g(1.1); // 同时满足 A、C → 选 C 版本
g(std::unique_ptr<int>()); // 只满足 A → 选 A 版本
}

这里 C<T> = A<T> && B<T> 明显比 A<T> 更“强”,所以当两者都能匹配时,编译器选 requires C<T> 那个。


🧰 7. Concepts 在真实代码中的实践

需求:只有 能被 **std::hash** 处理,且结果可转成 **std::size_t** 的类型,才能作为 hashmap 的 key。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <cstddef>
#include <concepts>
#include <functional>
#include <string>

// HashType 概念:
// 对于 T 类型的 a,std::hash<T>{}(a) 必须可以编译且结果可转成 std::size_t
template<typename T>
concept HashType = requires(T a) {
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
};

// 使用概念约束 key 类型

template<HashType K, class HashFunc = std::hash<K>>
class hashmap {
public:
bool insert(const K& key) {
size_t hashi = HashFunc()(key);
// ... 真实插入逻辑
return true;
}
};

int main() {
hashmap<int> hm1; // OK
hashmap<std::string> hm2; // OK
// hashmap<std::pair<std::string, int>> hm3; // 编译失败:不满足 HashType
}

目标:只允许“存放整数且可输出”的容器调用 print_range

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <concepts>
#include <iostream>
#include <vector>
#include <mutex>

// 对 T 的要求:
// 1)有 begin()/end(),返回 iterator 类型
// 2)value_type 是整型
// 3)value_type 能被 std::cout 输出

template<typename T>
concept PrintableRange = requires(T t) {
{ t.begin() } -> std::same_as<typename T::iterator>;
{ t.end() } -> std::same_as<typename T::iterator>;
requires std::integral<typename T::value_type>;
requires requires(typename T::value_type e) { std::cout << e; };
};

// 只允许 PrintableRange 调用

template<PrintableRange R>
void print_range(const R& r) {
for (const auto& e : r) {
std::cout << e << &#x27; &#x27;;
}
std::cout << &#x27;n&#x27;;
}

int main() {
std::vector<int> v{1, 2, 3};
print_range(v); // OK

std::vector<std::mutex> vm;
// print_range(vm); // 编译失败:mutex 既不是整型,也不能直接输出
}

用 Concept 限制“假迭代器”匹配到迭代器构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <concepts>

// 迭代器概念:只要求支持 ++ 和 *

template<typename T>
concept IteratorType = requires(T it) {
++it;
*it;
};

namespace zdl{

template<class T>
class vector {
public:
using iterator = T*;

vector() = default;

// 迭代器区间构造(加 concept 后更安全)
template<IteratorType InputIterator>
vector(InputIterator first, InputIterator last) {
while (first != last) {
push_back(*first);
++first;
}
}

vector(size_t n, const T& val = T()) {}

void push_back(const T& x) {}

private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _endofstorage = nullptr;
};

} // namespace bit

int main() {
bit::vector<int> v7(10, 1);
bit::vector<size_t> v9(10, 1);
}

没有 IteratorType 时,bit::vector<int> v7(10, 1); 这种调用有可能被错误匹配为“迭代器区间构造”,加上 Concept 就能让语义更明确,避免二义性。


🆚 8. Concepts 与 SFINAE 的关系

  • SFINAESubstitution Failure Is Not An Error

靠模板替换失败来“静默”排除候选

  • 常见工具:std::enable_ifdecltype

  • 代码冗长、晦涩,错误信息非常难读

Concepts

  • 直接用 concept 声明语义约束,如“整型可比较”“可哈希”等

  • 写法接近自然语言,可读性和可维护性都更好

  • 是 SFINAE 的“官方替代方案”,大幅简化模板元编程

对比示例(同一个需求:约束 T 为整型):

1
2
3
4
5
6
7
8
9
// C++17:SFINAE 写法

template<typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void foo(T x) {}

// C++20:Concept 写法

template<std::integral T>
void foo(T x) {}

C++20 的版本语义清晰到“几乎不需要注释”,这就是 Concept 的最大价值之一。


✅ 9. 小结:写 Concept 时可以记住的 checklist

  • 先问自己:我希望 T 满足什么语义?

是否是某类类型?(整型 / 浮点 / 容器 / 迭代器 …)

  • 是否支持某些操作?(+++size()begin()/end() …)

  • 表达式的返回类型、是否 noexcept 有要求吗?

优先用标准库现成概念

  • std::integralstd::floating_pointstd::regularstd::ranges::range

自定义概念时

  • 简单属性 → 用 std::is_xxx_v + requires

  • 操作合法性 → 用 requires (T t) { 表达式… }

  • 复杂组合 → 用逻辑与 / 或 / 非 + 嵌套 requires

在模板中使用时

  • 写在模板参数列表:templat<MyConcept >

  • 或用 requires 子句:template<typename T> requires MyConcept<T>

  • 或约束 autovoid f(MyConcept auto x)

建议你在自己的代码里做一件事

把最常用的那几个模板函数(比如工具类、容器包装、算法函数)都加上 1~2 个简单的 Concept 约束,感受一下错误信息和可读性发生的变化。