
文章目录一、Concept1.1 传统模板的问题1.2 Concept 参与重载决议的方式1.3 Concept和模板特化二、Range它解决了 STL 哪些痛点使用场景核心适配器三、Coroutine协程是什么三个关键字三个底层角色协程解决了哪些问题局限四、Module为什么编译更快阻碍五、constexpr 和 consteval六、 三路比较一、Concept1.1 传统模板的问题先看一段 C17 代码这是一个泛型加法函数templatetypenameTTadd(T a,T b){returnab;}调用add(1, 2)没问题。但如果你不小心写了add(hello, world)编译器会给你吐出一座富士山般的错误信息满屏都是std::__1::operator的匹配失败细节真正有用的信息被淹没在几千行模板实例化堆栈里。原因“hello” 和 “world” 的类型不是string而是 const char*C 不支持两个裸指针相加const char* const char* 没有定义编译器直到实例化阶段才会深入函数体内部去检查 a b 是否合法。当它尝试用 const char* 去匹配 operator 时会遍历所有重载候选最终失败。但此时它会将整个模板实例化过程中的“遗言”全部吐出来导致报错极长。C20 用 Concept 给模板参数加了一道安检门#includeconceptstemplatetypenameTrequiresstd::integralT// T 必须是有符号或无符号整数类型Tadd(T a,T b){returnab;}更简洁的写法template std::integral T或者带 auto 的缩写auto add(std::integral auto a, std::integral auto b)现在调用add(hello, world)编译器直接报const char*不满足integral概念错误信息清晰到小学生都能看懂。你可能会有疑问这不是失去了模板的意义了吗事实上“模板的意义” 并不是 “能接受任意类型”。在现代C以及所有静态类型语言中模板的真正意义是在不损失类型安全的前提下为多种类型编写同一份算法而不是“盲目接受全世界”。templatestd::integral TTadd(T a,T b){returnab;}add(1,2);// ✅ int满足 integraladd(1L,2L);// ✅ long满足 integraladd((short)1,2);// ✅ short满足 integraladd(1u,2u);// ✅ unsigned int满足 integral1.2 Concept 参与重载决议的方式Concept 不仅是编译期断言它还参与重载优先级。看这个例子#includeconcepts#includeiostream// 版本1接受任意类型voidprocess(autox){std::coutgeneral version\n;}// 版本2只接受整数类型更特化voidprocess(std::integralautox){std::coutintegral version\n;}intmain(){process(42);// 输出 integral version因为 integral 约束更严格process(3.14);// 输出 general version}编译器在做重载决议时约束越强的版本优先级越高。这背后是一个叫偏序约束的机制C20 标准明确定义了哪个约束更严格的比较规则。1.3 Concept和模板特化Concept 和 模板特化 是两种正交的机制它们解决不同的问题但可以配合使用。机制核心作用何时生效Concept约束——限制哪些类型可以被模板接受编译期选择模板之前作为“门卫”模板特化定制——为特定类型提供不同的实现编译期选择模板之后作为“分支”调用 add(1, 2) ↓ 【Concept 检查】std::integralint ? ✅ 通过 ↓ 【模板实例化】使用主模板或特化版本 ↓ 【特化匹配】如果有 int 的特化走特化否则走主模板#includeconcepts#includeiostream#includestring// 主模板约束为整型templatestd::integral TTadd(T a,T b){std::cout主模板整型: ;returnab;}// 特化版本 1针对 int 的特殊处理templateintaddint(inta,intb){std::cout特化版本int 专用: ;returnab100;// 故意多加 100演示特化生效}// 特化版本 2针对 long 的特殊处理templatelongaddlong(longa,longb){std::cout特化版本long 专用: ;returna*b;// 乘法演示特化可以完全不同}intmain(){std::coutadd(1,2)\n;// 走特化int 专用std::coutadd(1L,2L)\n;// 走特化long 专用std::coutadd((short)1,(short)2)\n;// 走主模板short// add(hello, world); // ❌ 编译失败const char* 不满足 integralreturn0;}输出特化版本int 专用: 103 特化版本long 专用: 2 主模板整型: 3Concept 和特化的“博弈”场景特化版本可以“绕过”Concept 的约束吗不能。Concept 在入口处就拦截了不符合要求的类型templatestd::integral TTadd(T a,T b){returnab;}templatedoubleadddouble(doublea,doubleb){returnab;}// ❌ 编译失败double不满足std::integral所以主模板的 Concept 检查直接拒绝特化连上场的机会都没有。特化版本可以“强化”约束吗可以。你可以为一个已经受 Concept 约束的模板进一步限制其特化版本templatestd::integral TTadd(T a,T b){returnab;}// 只针对 signed int 做特化更严格的约束templatestd::signed_integral T// 注意这是 C20 允许的约束特化Tadd(T a,T b){// 只有 int、long、short 等有符号整型走这里returnab1;}约束特化是 C20 引入的新特性在 C17 及之前只能通过std::enable_if模拟。偏特化与 Concept 的互动“对于类模板偏特化可以和 Concept 结合。例如templatetypenameTclassMyClass;// 偏特化只针对满足 std::integral 的类型templatestd::integral TclassMyClassT{/* ... */};这种写法在 C20 中更自然替代了之前用std::enable_if的复杂写法。”二、RangerangesC20 给 STL 新增的一套区间流水线框架。核心三块Range代表一段数据区间vector、数组、字符串都是 rangeView对原数据做的一个滤镜窗口不拷贝数据、惰性求值、复制成本极低管道|把多个操作串成流水线数据源 | 过滤 | 变换 | 截取可以像写数据流管道一样处理容器一步接一步加工数据。它解决了 STL 哪些痛点老 STL 算法std::sort(begin, end)每次都要传一对迭代器啰嗦不能链式调用过滤 → 转换 → 截取中间必须新建临时 vector 存结果多内存开销立刻执行计算没有惰性没遍历就已经算出全部结果begin和end迭代器类型必须一模一样写不出无限序列、条件终止的数据流Ranges 的解法算法直接接收容器不用写begin/end视图适配器链式组合不需要中间容器惰性求值只有你循环遍历的时候才真正开始计算哨兵(Sentinel)end 可以和 begin 类型不一样支持无限数据流额外附赠能力ranges 算法自带投影(projection)排序/查找时可以便捷按成员字段处理。使用场景多步流式数据处理筛选→转换→截取→跳过元素链式写一串管道autoresvec|views::filter(偶数)|views::transform(x*10)|views::take(5);不想产生中间临时容器节省内存尤其大数据生成无限序列比如从0开始无限递增数字搭配 take 截断容器按结构体成员快速排序、查找投影功能懒加载数据流遍历一点算一点不需要一次性生成全部数据核心适配器views::filter过滤元素views::transform变换元素views::take / drop取前N个 / 跳过前N个views::iota生成连续数字序列视图不延长原数据生命周期临时对象直接生成视图会悬垂惰性求值每次for循环遍历视图都会重新跑一遍流水线三、Coroutine如果你有 10000 条IO任务等待网络就要开10000个线程。线程成本高内存栈、操作系统上下文切换开销资源扛不住。协程是什么协程 可以中途停下来之后接着上次停下的位置继续跑的函数。举一个生活化比喻普通函数烧水 → 洗菜 → 炒菜。一口气做完不能中途暂停。协程函数烧水等待水烧开停下来去做别的事→ 水开了回来继续洗菜 →等待菜泡软又停下来→ 回来炒菜。当协程遇到等待网络、定时器就主动让出当前线程。线程可以转身去跑其他任务。等等待事件完成后再找一个线程回来继续执行这个协程。C20只给了你“暂停/恢复函数”的语法没有自带调度器也就是语言告诉你怎么停下但不会自动安排谁、什么时候把你唤醒恢复。唤醒工作必须你自己或者第三方库Asio、Folly实现。Go goroutine 对比Go 是语言自带调度器 有栈协程开出来就能跑。C20协程无栈协程 裸语法像买到一堆零件需要自己拼发动机调度器。协程暂停那一刻当前线程上的函数调用栈全部销毁线程自由了函数所有需要保存的数据局部变量、参数、暂停在哪一行全部放到堆内存的协程帧恢复的时候重新建立线程栈跳到暂停的那一行往下执行。三个关键字co_await等待协程最核心指令在这里暂停等待某个事情做完再往后走。autorespco_awaithttp_request(xxx);程序停在这里线程解放去干别的活网络返回之后再回来执行后面代码。co_yield产出一个值暂停函数返回一个结果给调用者下次恢复继续往下执行适合生成器循环产出序列按需生成不一次性占满内存co_return协程任务结束普通return不能在协程函数中使用。三个底层角色Promise指挥官协程内部的控制器协程刚启动、挂起、返回结果、抛出异常全部由Promise里面的函数钩子决定怎么做Awaiter暂停说明书当你写co_await XX最终会变成一个Awaiter对象这份说明书回答3个问题要不要现在暂停如果暂停了暂停以后干什么交给调度器恢复以后给协程返回什么结果std::coroutine_handle遥控器一个操作协程的手柄。外部拿着手柄可以resume()唤醒协程、判断任务终点、销毁协程释放堆内存。协程解决了哪些问题消灭回调地狱异步代码“同步化书写”旧回调嵌套/链式回调代码被切成碎片协程从上到下线性书写if/for/while都可以正常用可读性大幅提升IO密集场景大幅减少线程资源消耗10000条网络任务不需要10000个线程。IO等待期间协程挂起线程空闲复用少量线程就可以管理巨量任务。原生支持懒生成器流式产出数据不需要一次性生成全部结果放入内存。一边遍历一边生成数值适合超大文件、数据流。方便书写长周期状态机等待传感器信号就绪 → 执行动作 → 等待下一个信号。用普通函数你需要手动写一堆状态变量记录自己执行到哪一步协程直接暂停在等待处位置自动保存。局限不会自动多核并行。协程本质协作式让出。如果你代码一直循环计算不写co_await主动暂停线程会被一直霸占。想要多核仍然需要std::thread。C20没有开箱即用异步任务库。不能写完co_await直接跑必须搭配Asio/Folly/自研Task框架。不适合CPU密集运算。CPU一直忙几乎没有等待协程没有收益。四、ModuleModule 是 C20 引入的一种新的编译单元用于替代传统的头文件.h/.hpp 源文件.cpp的分离模式旨在解决头文件重复编译、宏污染、编译速度慢等历史遗留问题。Module 的语法其实简单难的是理解和迁移。基本写法一个模块分为接口文件.ixx或.cppm和实现文件// math.ixxexportmodulemath;// 声明模块名exportintadd(inta,intb){// 导出函数returnab;}exportnamespacealgebra{intmultiply(inta,intb){returna*b;}}使用端// main.cppimportmath;// 导入模块不需要头文件了importstd;// C23 支持C20 得单独 import 具体标准库模块intmain(){autoresultadd(3,4);return0;}为什么编译更快传统#include的工作方式是文本复制#include vector会把整个 vector 头文件可能上万行复制到每个.cpp里然后预处理器展开、编译器编译Module 的工作方式是编译一次导出符号表编译器编译math.ixx生成一个模块二进制接口这里面包含了模块导出的所有符号和类型信息。导入端import math;时编译器直接读取这个 BMI不需要重新解析和编译模块源码。BMI 是独立的编译产物不再依赖头文件的宏展开顺序宏污染问题自然消失。增量编译的提升更明显修改一个模块的内部实现只有依赖这个模块的.cpp需要重编译而传统头文件改一个头文件所有包含它的.cpp都得重编译。阻碍Module 目前最大的问题不是语言本身而是工具链的碎片化MSVC支持最好VS 2022 17.5 直接支持.ixx文件不需要额外配置。Clang16 支持但需要-stdc20 -fmodules以及-fmodule-file手动指定依赖。GCC14 开始支持但实验性很强。CMake3.28 开始支持add_library(math MODULE math.ixx)但各编译器实现细节不同CMAKE_CXX_MODULE相关配置还需要仔细调。迁移策略不要试图把整个项目一次性切模块。从最底层的工具库开始如string_utils、math_utils这些库依赖少、封装好切模块收益高、风险低。然后逐步向上层模块迁移底层模块可以被上层通过import引用。五、constexpr 和 constevalC20 之前constexpr函数里不能使用动态内存分配。现在放宽了#includevector#includealgorithmconstexprintcomputeSum(){std::vectorintv{1,2,3,4,5};std::ranges::sort(v);// C20 的 ranges 算法也是 constexpr 的intsum0;for(intx:v)sumx;returnsum;}// 编译期完成计算最终二进制里就是个常量 15static_assert(computeSum()15);consteval更进一步它强制函数在编译期执行如果做不到直接编译错误constevalintsquare(intn){returnn*n;}intxsquare(5);// OK编译期算好了inty42;intzsquare(y);// 错误y 是运行时变量无法在编译期求值constexpr和consteval的区别常被误解constexpr是尽可能在编译期执行不行就退化为运行期而consteval是必须在编译期执行不行就报错设计上consteval更严格适合用于编译期哈希、编译期字符串处理等场景。六、三路比较写一个简单的Point类#includecomparestructPoint{intx,y;autooperator(constPoint)constdefault;// 自动生成所有比较运算符};intmain(){Point a{1,2},b{1,3};boollessab;// 按 x 优先x 相等再比 y返回 truebooleqab;// 自动生成返回 false} default会按成员声明顺序逐个比较遇到第一个不等的成员就返回结果。强类型返回值返回的不是int而是一个排序类别ordering categorystd::strong_ordering完全可替换的相等如整数相等意味着完全一致。std::weak_ordering等价值可以互换但不一定完全等价如不区分大小写的字符串。std::partial_ordering存在不可比较的值如浮点数的 NaN。当你显式定义了operator编译器不会自动生成operator你得同时定义或 default。反过来如果你定义了operator编译器会自动生成operator!。