现在让我们设好番茄钟放一首好听的音乐开始学习吧 🌈 😋
🧠 导读:这篇文章怎么读?
- 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 T、class T,几乎没有语义信息 - 约束主要靠
<strong>SFINAE / enable_if / decltype</strong>等技巧 - 一旦写错类型,编译器报一大堆“读不懂”的模板错误
在 C++20 之后:
- 用
concept定义编译期谓词(返回bool的条件) - 使用概念直接约束模板参数,不满足就不匹配模板
- 错误信息会以“哪个 concept 不满足”的形式出现,可读性大幅提升
简单一句话:
Concept = 编译期布尔表达式 + 模板参数语义标签
🧱 2. 如何定义一个概念?(基础语法)
标准形式:
// template-parameter-list 是模板参数列表
// concept-name 是概念名
// constraint-expression 是可求值为 bool 的常量表达式
template <template-parameter-list>
concept concept-name = constraint-expression;
课件里的几种典型写法:
#include <type_traits>
template<typename T>
concept Integral = std::is_integral_v<T>;
含义:T 必须是整型(int/long/char 等)。
template<typename T>
concept SmallType = sizeof(T) <= 4;
含义:T 的大小 ≤ 4 字节。
template<typename T>
concept Incrementable = requires(T t) {
++t; // 检查前缀++是否有效
t++; // 检查后缀++是否有效
};
含义:T 必须支持前后缀自增。
🔍 3. requires 表达式的四种形式
课件给了一个框架:
template<typename T>
concept ConceptName = requires (/* parameter-list 可选 */) {
/* requirement-seq */
};
在 requires \\\{ ... \\\} 里面,可以出现四类“要求”:
只检查“这条表达式能不能编译过”。
#include <concepts>
template<typename T>
concept Addable = requires(T a, T b) {
a + b; // 只要 a + b 这句能编译就行
};
检查某个嵌套类型是否存在:
template<typename T>
concept HasValueType = requires {
typename T::iterator; // 必须有 iterator
typename T::value_type; // 必须有 value_type
};
同时约束“表达式能否编译 + 返回类型 + noexcept 与否”。
语法:
{ 表达式 } [noexcept] -> 类型约束;
示例:
#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,放更复杂的逻辑条件:
#include <type_traits>
template<typename T>
concept ComplexConcept = requires(T t) {
requires sizeof(T) <= sizeof(long); // 额外静态布尔条件
requires std::is_class_v<T>; // 必须是类类型
};
🧪 4. 概念在模板里的几种用法
课件总结了 5 种常见用法:
#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;
}
调用:
f1(1); // OK
f1("xxx"); // 编译期报错:不满足 Integral
template<typename T>
requires Integral<T>
void f2(T x) {}
template<typename T>
void f3(T x) requires Integral<T> {}
效果与 4.2 相同,只是写法不一样。
适用于“只在这个函数体里临时需要的条件”:
template<typename T>
requires requires(T x) { x.size(); x + x; }
void f4(T x) {}
含义:T 必须有 <strong>size()</strong> 成员,并且 <strong>x + x</strong> 可行。
void f5(Integral auto x) {}
比起泛写 auto,这里要求参数必须是整型。
🔗 5. 约束表达式的逻辑组合
概念本质上是“返回 bool 的约束表达式”,自然可以用逻辑运算:
- 单个概念:
std::integral<T> - 单个
requires表达式 - 单个
constexpr bool表达式
所有约束都为真,整体才为真。
#include <concepts>
#include <iostream>
using std::cout;
// 必须既是整数,又能输出
template<typename T>
concept IntegralAndPrintable = std::integral<T> && requires(T t) {
cout << t;
};
至少一个约束为真,整体就为真。
template<typename T>
concept Number = std::integral<T> || std::floating_point<T>;
template<typename T>
concept NonPointer = !std::is_pointer_v<T>;
对参数包做“约束折叠”:
#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。
课件示例:
#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"); // 只匹配无约束版本
}
再看更复杂一点的:
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 在真实代码中的实践
需求:只有 能被 <strong>std::hash</strong> 处理,且结果可转成 <strong>std::size_t</strong> 的类型,才能作为 hashmap 的 key。
#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:
#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 << ' ';
}
std::cout << 'n';
}
int main() {
std::vector<int> v{1, 2, 3};
print_range(v); // OK
std::vector<std::mutex> vm;
// print_range(vm); // 编译失败:mutex 既不是整型,也不能直接输出
}
用 Concept 限制“假迭代器”匹配到迭代器构造函数。
#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 的关系
- SFINAE(Substitution Failure Is Not An Error)
- 靠模板替换失败来“静默”排除候选
- 常见工具:
std::enable_if、decltype等 - 代码冗长、晦涩,错误信息非常难读
- Concepts
- 直接用
concept声明语义约束,如“整型可比较”“可哈希”等 - 写法接近自然语言,可读性和可维护性都更好
- 是 SFINAE 的“官方替代方案”,大幅简化模板元编程
- 直接用
对比示例(同一个需求:约束 T 为整型):
// 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::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)
- 写在模板参数列表:
✅ 建议你在自己的代码里做一件事:

