68、静态断言:static_assert 的用法

说到断言,很多嵌入式工程师第一反应是 assert() 宏。运行时断言嘛,程序跑起来之后检查条件,不满足就 abort。但有个问题——有些错误,其实在编译阶段就能发现。你想想看,如果等到程序跑起来才发现一个本可以在编译时就捕获的错误,那多浪费调试时间?

这就是 static_assert 的用武之地。它是在编译期执行的断言,条件不满足就直接编译失败。我个人习惯在写底层驱动、协议栈、或者做跨平台移植时大量使用它。说白了,就是把「运行时才能发现的坑」提前到「按编译按钮的那一刻」就暴露出来。

什么是 static_assert?

static_assert 是 C11 标准引入的关键字(C++ 也有,但语法略有不同)。它的作用是在编译时检查一个常量表达式是否为真。如果为假,编译器会报错,并输出你指定的错误信息。

基本语法:

static_assert(常量表达式, "错误信息");

注意:这里的表达式必须是编译期能确定的常量,不能是变量。比如你不能写 static_assert(x > 0, "x must be positive"),因为 x 是运行时变量,编译器不知道它的值。

为什么需要它?

我举个例子。我在项目中曾经遇到过一个问题:某个通信协议要求数据包长度必须是 4 字节对齐。结构体定义好了,但不同编译器下结构体的对齐方式不一样。有人改了结构体成员顺序,结果 sizeof 变成了 6,不是 4 的倍数。程序跑起来后,数据包解析全乱套了。

如果当时用了 static_assert

struct Packet {
    uint8_t  type;
    uint16_t length;
    uint8_t  payload[3];
};

static_assert(sizeof(struct Packet) % 4 == 0,
              "Packet struct must be 4-byte aligned");

编译时就会直接报错,根本不会让你生成可执行文件。嗯,这就是它的价值——把隐患扼杀在摇篮里。

典型应用场景

我总结了几个我在实际项目中经常用 static_assert 的地方:

  • 检查数据类型大小:比如确保 int 是 4 字节,long 是 8 字节。跨平台移植时特别有用。
  • 检查结构体对齐:上面那个例子就是。
  • 检查枚举值范围:确保枚举不会溢出。
  • 检查数组长度:比如两个表必须等长。
  • 检查宏定义合法性:比如某个配置宏必须在特定范围内。

来看一个更完整的例子:

#include <stdint.h>
#include <stddef.h>

// 假设我们有一个硬件寄存器映射结构体
typedef struct {
    uint32_t ctrl;     // 控制寄存器
    uint32_t status;   // 状态寄存器
    uint32_t data;     // 数据寄存器
    uint32_t reserved; // 保留
} HW_Regs;

// 确保结构体大小正好是 16 字节(4 个 32 位寄存器)
static_assert(sizeof(HW_Regs) == 16,
              "HW_Regs size mismatch, check alignment");

// 确保偏移量正确
static_assert(offsetof(HW_Regs, status) == 4,
              "status register offset should be 4");

// 检查某个宏是否在合理范围内
#define MAX_PACKET_SIZE 1024
static_assert(MAX_PACKET_SIZE > 0 && MAX_PACKET_SIZE <= 4096,
              "MAX_PACKET_SIZE out of range");

你看,这些检查全部在编译期完成。如果硬件手册更新了寄存器布局,或者你换了编译器导致对齐方式变了,编译就会失败,而不是等到程序跑起来才 crash。

与运行时 assert 的区别

特性 static_assert assert()
执行时机 编译期 运行时
条件要求 必须是常量表达式 可以是变量表达式
失败后果 编译失败,不生成目标文件 程序 abort,可能产生 core dump
适用场景 类型大小、结构体布局、宏定义等 函数参数检查、指针非空检查等
性能开销 无(编译期完成) 有(运行时检查)

避坑指南

我曾经踩过一个坑:在 C89 标准下用了 static_assert,结果编译器直接报语法错误。因为 static_assert 是 C11 才引入的。如果你还在用老旧的编译器(比如某些嵌入式 IDE 自带的),需要确认是否支持 C11。

如果不支持,可以用一个变通方案——利用数组大小为负会编译失败的特性:

#define STATIC_ASSERT(cond, msg) \
    typedef char static_assert_##msg[(cond) ? 1 : -1]

// 使用示例
STATIC_ASSERT(sizeof(int) == 4, int_must_be_4_bytes);

这个技巧在 C89 环境下也能用。不过我个人建议,如果条件允许,尽量用标准的 static_assert,因为错误信息更清晰。

知识体系图

下面这张图总结了 static_assert 在整个断言体系中的位置,以及它的核心应用场景:

static_assert 知识体系 断言 (Assertion) 编译期断言 运行时断言 static_assert (C11) 宏技巧 (数组大小 -1) assert() 宏 应用场景: 类型大小检查 | 结构体对齐 | 枚举范围 | 宏合法性 应用场景: 参数检查 | 指针非空 | 资源状态 | 逻辑前置条件

总结

static_assert 是一个被很多人忽略但非常实用的工具。它把「事后诸葛亮」变成了「事前预防针」。我建议你在以下场景优先考虑它:

  • 任何依赖特定数据类型大小的代码
  • 结构体布局有对齐或偏移量要求时
  • 跨平台移植时,检查平台相关的假设
  • 配置宏的合法性验证

记住一句话:能在编译期解决的问题,就不要拖到运行时。这不仅节省调试时间,也让代码更健壮、更可维护。

小技巧:在头文件中使用 static_assert 检查公共数据结构,这样所有包含该头文件的源文件都会受益。一次检查,处处安心。
注意:static_assert 的第二个参数(错误信息)必须是字符串字面量,不能是变量或拼接的字符串。这是 C 标准的规定。

公众号:蓝海资料掘金营,微信deep3321