C23 的新特性

本文最后更新于 2026年10月9日

最近读C++26 要来了——C++ 标准委员会会议初体验 - 吴咏炜的文章 - 知乎时,发现了一个 C23 很好玩的新 feature:_BitInt,于是顺手研究了一下 C23 都有哪些新东西。

_BitInt:任意位宽的整数

以前我们想要一个 3 位的整数,只能用位域(bit-field)凑合,而且位域有一堆限制(不能取地址、布局由实现定义)。想要 128 位整数,得靠编译器扩展 __int128。想要 4096 位的密码学大整数?老老实实上 GMP。

C23 用 _BitInt(N) 统一了这一切:

1
2
3
_BitInt(3)          a;   // 有符号 3 位整数,范围 [-4, 3]
unsigned _BitInt(3) b; // 无符号 3 位整数,范围 [0, 7]
unsigned _BitInt(128) c; // 128 位无符号整数,不用 __int128 了

规则很简单:

  • 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
2
3
_BitInt(3) x = 3;  // 最大值
x += 1; // 回绕到 -4
printf("%d\n", (int)x); // 输出 -4

不做整数提升

这是 _BitInt 和普通小整数类型最大的区别。char + char 会先提升成 int 再相加,但 _BitInt 被豁免了整数提升,运算保持原位宽:

1
2
3
4
5
_BitInt(3) x = 3;
sizeof(x + x); // 1,类型仍是 _BitInt(3)

signed char c = 1;
sizeof(c + c); // 4,char 被提升成了 int

这一点对模拟硬件行为很重要——比如你想精确模拟一个 12 位 ADC 或者 FPGA 里某个奇数位宽的寄存器,整数提升会悄悄改变运算语义,_BitInt 不会。

wb 字面量后缀

42wb 是一个 _BitInt 字面量,位宽取能容纳该值的最小值(有符号要算符号位,所以 42wb 的类型是 _BitInt(7)):

1
2
auto lit = 42wb;
sizeof(lit); // 1

printf 没有对应格式符

标准没给 _BitInt 加新的 printf 格式符,打印时需要先转换:

1
2
3
unsigned _BitInt(128) big = 1;
big <<= 100;
printf("%llu\n", (unsigned long long)big); // 得手动截到能打印的类型

128 位以上的值想完整打印,得自己写循环按 64 位一块块拆。

用途

  • 密码学 / 大整数:ECC 常用的 256 位、521 位(对,P-521 就是非对齐位宽)运算,以前要么手写多精度,要么靠编译器扩展拼;
  • 硬件模拟 / 嵌入式:HDL 里常见的 17 位、576 位这种”奇怪”位宽,现在可以直接表达;
  • 文件格式解析:某些协议字段不是字节对齐的,配合位域可以更自然地建模。

其他好玩的 C23 特性

#embed:把文件塞进二进制

预处理器可以直接把任意文件按字节展开成逗号分隔的整数列表:

1
2
3
4
static const unsigned char logo[] = {
#embed "logo.png"
};
// logo 就是 PNG 文件的全部字节,sizeof(logo) 就是文件大小

还支持 limit、prefix、suffix、if_empty 等参数,这个功能也进了 C++26。以前在编译期内嵌二进制文件的各种 trick(xxd -i、incbin、objcopy),我在另一篇文章里专门写过,这里就不展开了。

auto 类型推导

对,C 也有 auto 了,而且是 C++ 那种类型推导(原来的”自动存储期”含义因为本来就是默认值,直接被挤掉了):

1
2
auto i = 42;      // int
auto d = 3.14; // double

限制是必须单个声明、必须有初始化式,auto a = 1, b = 2; 是不行的。

constexpr、nullptr、原生 bool

C 继续从 C++ 进货:

1
2
3
constexpr int N = 1'000'000;  // 真正的编译期常量,比 enum hack 和宏都干净
int *p = nullptr; // 类型是 nullptr_t,不再依赖 NULL 宏的实现细节
bool b = true; // 不用 #include <stdbool.h> 了

顺带一提,数字分隔符 ' 和二进制字面量 0b1010 也是 C23 加的。

typeof

GCC 扩展进了标准,我之前专门写过一篇,这里不重复了。值得一提的是随同标准化的还有 typeof_unqual,用于去掉限定符(移除 const、volatile、atomic 等限定符):

1
typeof_unqual(const int) k;  // 类型是 int,不带 const

写泛型宏时很有用——typeof 会把 const 原样带出来,而临时变量往往不想要的这个限定符。

enum 可以指定底层类型

1
2
enum Color : unsigned char { RED, GREEN, BLUE };
sizeof(enum Color); // 1

以前 enum 的底层类型由编译器挑选,不同编译器 / 不同 ABI 下可能不一样,做二进制协议时很头疼。现在可以钉死。

<stdckdint.h>:溢出检查算术

1
2
3
#include <stdckdint.h>
int r;
bool ovf = ckd_add(&r, INT_MAX, 1); // ovf == true,r 里是回绕结果

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 实现成熟得晚,生态还没跟上。