C++的核心:硬件暴力美学和零成本抽象!
C++的核心:硬件暴力美学和零成本抽象!
kleekwaii有一个专门算浮点乘加的函数:std::fma,简写来自 Fused Multiply-Add;你输入 x, y, z,然后它给你返回 x * y + z。有人可能会问:“哎?那这和直接写 x * y + z 有什么区别呢?”这个函数真正关键的其实是那个 Fused。
普通写法 x * y + z 会分两步走:先算 temp = x * y。在这个过程中,结果会被舍入到浮点数的可表示精度(例如 double 的 53 位有效数字)。这里丢失了一次精度。再算 result = temp + z,而这里又进行了一次舍入;总共有2 次舍入误差。
而 std::fma(x, y, z) 根据 IEEE 754 标准,必须像拥有无限精度一样计算 x * y + z 的确切值,然后只在最后将结果存回浮点数格式时,进行唯一的一次舍入。这意味着 std::fma 通常比普通乘加更精确。特别是在 x * y 的结果和 z 大小相近但符号相反发生灾难性抵消时,std::fma 能保留更多的有效位。
现在的现代 CPU(如 Intel 的 Haswell 架构以后,ARM Cortex-A 系列等)通常都在 ISA 层面直接支持 FMA 指令(如 x86 的 FMA3 或 FMA4 指令集)。std::fma 通常会被编译成一条汇编指令(例如 vfmadd213sd)。这意味着它不仅精度高,而且速度极快(通常只需 4-5 个时钟周期,吞吐量很高)。
但如果硬件不支持,编译器会调用软件库来模拟无限中间精度。这时候它会非常慢;标准定义了三个可选的宏:FP_FAST_FMA, FP_FAST_FMAF, FP_FAST_FMAL 来表明地面实况。
想象一下你写了一个std::list
在 C++11 之前(比如 C++98),强制要求每个 allocator 都要手写一个 rebind 结构体;但是到了 C++11 及以后,会通过 std::allocator_traits 的 SFINAE 技巧来检测用户有没有手写 rebind,如果有就用用户自定义,如果没有就自动替换模板参数。
什么是 SFINAE 呢?Substitution Failure Is Not An Error:如果在替换模板参数的过程中产生了无效的代码(比如访问不存在的类型),编译器不会立刻报错,而是认为这个重载不匹配,继续寻找下一个;但是SFINAE 只保护函数签名部分的替换失败,包括返回值类型、函数参数类型、模板参数默认值,但是不包括: 函数体内部的代码。
第一代,最经典的SFINAE 工具便是 std::enable_if。它的原理是利用偏特化:如果条件为真,它就有一个 type 成员;如果为假,它就没有 type 成员(从而触发 SFINAE,让函数消失)。请看 Gemini 为我生成的伪代码:
1 | [#]include <type_traits> |
1 |
1 | // 版本 1:只在这个 T 是浮点数时才存在 |
里面的那个 typename 单纯是C++语法问题,因为在模板实例化之前编译器默认作用域解析运算符后面那玩意是一个变量或者值。那么为什么有的时候有有些时候没有呢?核心是 Dependent Names,即编译器不能消除歧义。比如 std::vector
1 | template <typename T> |
(t[.]foo是因为会被识别成链接)
到了 C++17,我们有了更优雅的技巧,专门用来探测“这个类有没有某个成员”。
std::void_t<…> 的作用是:不管你往里面塞什么类型,只要它是有效的,结果就是 void。如果无效,就触发 SFINAE。Gemini 例子时间(检测一个类有没有 reserve() 函数):
1 | [#]include <type_traits> |
std::declval
1 | template<typename T> |
在 C++11 引入 decltype 之后,我们经常需要做这样的询问:“嘿,编译器,如果我有两个变量 x 和 y,让它们相加 x + y,得到的结果类型是什么?” 如果你有实例,这很好办;但是在模板元编程中,我们通常只有类型 T,没有实例。这个时候 decltype(std::declval
幸运的是,后来多态内存资源(PMR, Polymorphic Memory Resources)和 Concept 的引入终结了晦涩难懂的模板天书。
在 PMR 之前,Allocator 是容器类型的一部分。std::vector<int, AllocA> 和std::vector<int, AllocB> 是两个完全不同的类型!这意味着你不能把它们传给同一个函数,除非把函数也写成模板,导致代码膨胀严重,接口极度不灵活。std::pmr 利用虚函数和类型擦除的机制,把具体的内存分配策略藏在了运行时。现在 std::pmr::vector
以前用 enable_if 配合 SFINAE,写出来的代码满屏尖括号。一旦报错,编译器会吐出几千行的“模板实例化失败”堆栈,根本看不懂是哪里缺了条件。Concepts (编译期约束) 让我们直接用“自然语言”描述类型要求:
黑暗时代:
1 | template <typename T, |
光明时代:
1 | 模板<typename T, |
光明时代:
1 | void foo(std::integral auto t) { /*...*/ } |
总的来说,PMR 终结了 Allocator 导致的类型碎片化。它把复杂性从编译期类型系统转移到了运行时对象状态,让代码更像传统的 OOP。Concepts 终结了 SFINAE 的晦涩语法。它把隐晦的替换失败变成了显式的约束检查,让模板编程从黑魔法变成了常规工程。











