编程语言 cpp C++20的概念与约束 星野暗涌 2026-05-08 2026-07-04 🧠 导读:这篇文章怎么读?
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 之前:
在 C++20 之后:
简单一句话:
Concept = 编译期布尔表达式 + 模板参数语义标签
🧱 2. 如何定义一个概念?(基础语法) 标准形式:
1 2 3 4 5 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 () { };
在 requires \\\{ ... \\\} 里面,可以出现四类“要求”:
只检查“这条表达式能不能编译过”。
1 2 3 4 5 6 #include <concepts> template <typename T>concept Addable = requires (T a, T b) { a + b; };
检查某个嵌套类型是否存在:
1 2 3 4 5 template <typename T>concept HasValueType = requires { typename T::iterator; typename T::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 } noexcept -> std::same_as<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>;template <Integral T>void f1 (T x) { std::cout << "有 concepts 约束" << std::endl; }
调用:
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> template <class T >concept Integral = std::is_integral_v<T>;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 ); 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 ); g (std::unique_ptr <int >()); }
这里 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> template <typename T>concept HashType = requires (T a) { { std::hash<T>{}(a) } -> std::convertible_to<std::size_t >; }; 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; hashmap<std::string> hm2; }
目标:只允许“存放整数且可输出”的容器调用 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> 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; }; }; template <PrintableRange R>void print_range (const R& r) { for (const auto & e : r) { std::cout << e << ' '; } std::cout << 'n'; } int main () { std::vector<int > v{1 , 2 , 3 }; print_range (v); std::vector<std::mutex> vm; }
用 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 ; 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 ; }; } 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 的关系
SFINAE (Substitution Failure Is Not An Error )
靠模板替换失败来“静默”排除候选
Concepts
对比示例(同一个需求:约束 T 为整型):
1 2 3 4 5 6 7 8 9 template <typename T, typename = std::enable_if_t <std::is_integral_v<T>>>void foo (T x) {}template <std::integral T>void foo (T x) {}
C++20 的版本语义清晰到“几乎不需要注释”,这就是 Concept 的最大价值之一。
✅ 9. 小结:写 Concept 时可以记住的 checklist
是否是某类类型?(整型 / 浮点 / 容器 / 迭代器 …)
优先用标准库现成概念 :
std::integral、std::floating_point、std::regular、std::ranges::range 等
自定义概念时 :
简单属性 → 用 std::is_xxx_v + requires
操作合法性 → 用 requires (T t) { 表达式… }
复杂组合 → 用逻辑与 / 或 / 非 + 嵌套 requires
在模板中使用时 :
写在模板参数列表:templat<MyConcept >
或用 requires 子句:template<typename T> requires MyConcept<T>
或约束 auto:void f(MyConcept auto x)
✅ 建议你在自己的代码里做一件事 :
把最常用的那几个模板函数(比如工具类、容器包装、算法函数)都加上 1~2 个简单的 Concept 约束,感受一下错误信息和可读性发生的变化。