C23 的新特性
本文最后更新于 2026年10月9日
最近读C++26
要来了——C++ 标准委员会会议初体验 - 吴咏炜的文章 - 知乎时,发现了一个
C23 很好玩的新 feature:_BitInt,于是顺手研究了一下 C23
都有哪些新东西。
_BitInt:任意位宽的整数
以前我们想要一个 3
位的整数,只能用位域(bit-field)凑合,而且位域有一堆限制(不能取地址、布局由实现定义)。想要
128 位整数,得靠编译器扩展 __int128。想要 4096
位的密码学大整数?老老实实上 GMP。
C23 用 _BitInt(N) 统一了这一切:
1 | |
规则很简单:
N必须是整数常量表达式;unsigned _BitInt(N)要求N >= 1,有符号的_BitInt(N)要求N >= 2(得留一位给符号位,写_BitInt(1)会直接报signed _BitInt must have a bit size of at least 2);N的上限由实现定义,通过<limits.h>里的BITINT_MAXWIDTH查询,标准只要求至少为 64。实测 Apple Clang 17 是 128(有点抠门),上游 Clang 支持到 2^23 = 8388608,GCC 支持到 65535。
回绕行为
小位宽整数的溢出行为和位域一样是回绕的:
1 | |
不做整数提升
这是 _BitInt
和普通小整数类型最大的区别。char + char 会先提升成
int 再相加,但 _BitInt
被豁免了整数提升,运算保持原位宽:
1 | |
这一点对模拟硬件行为很重要——比如你想精确模拟一个 12 位 ADC 或者 FPGA
里某个奇数位宽的寄存器,整数提升会悄悄改变运算语义,_BitInt
不会。
wb 字面量后缀
42wb 是一个 _BitInt
字面量,位宽取能容纳该值的最小值(有符号要算符号位,所以
42wb 的类型是 _BitInt(7)):
1 | |
printf 没有对应格式符
标准没给 _BitInt 加新的 printf
格式符,打印时需要先转换:
1 | |
128 位以上的值想完整打印,得自己写循环按 64 位一块块拆。
用途
- 密码学 / 大整数:ECC 常用的 256 位、521 位(对,P-521 就是非对齐位宽)运算,以前要么手写多精度,要么靠编译器扩展拼;
- 硬件模拟 / 嵌入式:HDL 里常见的 17 位、576 位这种”奇怪”位宽,现在可以直接表达;
- 文件格式解析:某些协议字段不是字节对齐的,配合位域可以更自然地建模。
其他好玩的 C23 特性
#embed:把文件塞进二进制
预处理器可以直接把任意文件按字节展开成逗号分隔的整数列表:
1 | |
还支持
limit、prefix、suffix、if_empty
等参数,这个功能也进了 C++26。以前在编译期内嵌二进制文件的各种
trick(xxd -i、incbin、objcopy),我在另一篇文章里专门写过,这里就不展开了。
auto 类型推导
对,C 也有 auto 了,而且是 C++
那种类型推导(原来的”自动存储期”含义因为本来就是默认值,直接被挤掉了):
1 | |
限制是必须单个声明、必须有初始化式,auto a = 1, b = 2;
是不行的。
constexpr、nullptr、原生
bool
C 继续从 C++ 进货:
1 | |
顺带一提,数字分隔符 ' 和二进制字面量
0b1010 也是 C23 加的。
typeof
GCC 扩展进了标准,我之前专门写过一篇,这里不重复了。值得一提的是随同标准化的还有
typeof_unqual,用于去掉限定符(移除
const、volatile、atomic
等限定符):
1 | |
写泛型宏时很有用——typeof 会把 const
原样带出来,而临时变量往往不想要的这个限定符。
enum 可以指定底层类型
1 | |
以前 enum 的底层类型由编译器挑选,不同编译器 / 不同 ABI 下可能不一样,做二进制协议时很头疼。现在可以钉死。
<stdckdint.h>:溢出检查算术
1 | |
ckd_add / ckd_sub / ckd_mul
三个宏,类型通用。以前要手写一堆前置条件判断来防溢出,现在一步到位,而且通常能编译成直接读标志位的指令。
其他值得一提的
[[attributes]]语法进入 C:[[nodiscard]]、[[maybe_unused]]、[[fallthrough]]、[[deprecated]],noreturn也变成了属性;static_assert不再需要assert.h,而且支持单参数形式(省略消息);#elifdef/#elifndef终于来了,不用#elif defined(...);<stdbit.h>提供了一套位操作工具:stdc_count_ones、stdc_bit_width、stdc_rotate_left等;- K&R 函数定义被移除了,
int f()的含义从”参数未知”变成了”无参数”,一个 40 年历史遗留终结; memset_explicit标准化,专门对付编译器把”清密钥”的 memset 优化掉的问题。
谁在用这些特性
我很好奇这些新东西到底有没有人用,于是在 GitHub 上搜了一圈(排除编译器自身的测试用例),结果还挺有意思。
_BitInt 的真实用户
采用者比预想的多,而且集中在三类场景:
- 区块链的 256 位算术:Brave
浏览器的加密钱包直接
using uint256_t = unsigned _BitInt(256);做以太坊金额运算;Hyperledger Solang(Solidity 编译器)用它做 EVM 256 位整数的十进制格式化。 - 数据库的宽整数:ClickHouse
在 x86_64 上用
_BitInt(256)加速 SUM 聚合的 Int256 累加,甚至把Int8定义成signed _BitInt(8)来解决和char的混叠问题。 - 硬件建模:PandA-bambu(HLS
工具)拿它当任意位宽
ac_int的底层存储;Chips Alliance 的 t1(RISC-V 向量处理器)用unsigned _BitInt(1)、_BitInt(5)、_BitInt(12)表达中断标志、寄存器号、CSR 地址这些精确位宽的硬件信号。
另外 fmt
11.x 用它实现 128 位格式化,samurai(ninja
兼容构建工具)在 BITINT_MAXWIDTH >= 128 时用它替代
__uint128_t。
反过来,大家以为最该用的地方反而没用:OpenSSL、Mbed
TLS、libsodium 这些密码库零采用(尽管 P-521 这种 521
位曲线看起来是”天选场景”),Linux 内核、QEMU、CPython、SQLite
也都没有。原因不难理解:这些项目要兼容 MSVC 和老编译器,内核还锁在
gnu11,密码库对侧信道和新特性极度保守。而且已采用的项目几乎全是
Clang-only——GCC 的 _BitInt 实现成熟得晚,生态还没跟上。