资源受限MCU上的C++裁剪:禁用异常与RTTI后代码体积直降40%
资源受限MCU跑C++,体积膨胀往往不是class本身,而是异常栈展开表、RTTI的typeinfo、动态分配和未用模板实例被链进固件。实际工程里,把异常、RTTI、线程静态guard、未用段一起关掉,再配合LTO和gc-sections,带多态与STL小对象的工程常能把Flash缩减约40%;纯GPIO/状态机类较轻,降幅会小一些。
一、基础编译选项
ARM GCC裸机建议:
CXXFLAGS += -mcpu=cortex-m4 -mthumb -std=c++17 -Os
CXXFLAGS += -fno-exceptions -fno-rtti -fno-threadsafe-statics
CXXFLAGS += -ffunction-sections -fdata-sections
CXXFLAGS += -fno-unwind-tables -fno-asynchronous-unwind-tables
CXXFLAGS += -flto -Wall -Wextra
LDFLAGS += -Wl,--gc-sections
-fno-exceptions去掉try/catch、栈展开和__cxa_throw等运行时;-fno-rtti去掉dynamic_cast/typeid及每类typeinfo,多态类只保留虚表、不再附加类型名字符串。 -fno-threadsafe-statics避免局部static构造插入guard锁,裸机单线程/未起RTOS前更安全。 配合-ffunction-sections/-fdata-sections与--gc-sections,未调用函数、未用模板实例可被链接器删除。
二、用错误码替代throw
禁用异常后,错误走返回值。轻量Result可自己写:
enum class Err : uint8_t { Ok, Param, Busy, Crc, Timeout };
template<typename T>
struct Result {
T value; Err err;
constexpr bool ok() const { return err == Err::Ok; }
};
Result<uint16_t> read_reg(uint8_t id, uint16_t addr){
if(id >= 8) return {0, Err::Param};
uint16_t v = bus_read(id, addr);
if(crc_check(id, addr, v) == false) return {0, Err::Crc};
return {v, Err::Ok};
}
调用处必须判ok(),不会因throw未捕获进terminate。C++23可用std::expected,老工具链用上面这个零分配版本更稳。
三、用CRTP替代dynamic_cast
运行时类型查询在MCU里大多可编译期确定。基类模板转发到派生类,无虚表、无RTTI:
template<typename D>
struct SensorBase {
int read(uint8_t* buf, uint16_t len){
return static_cast<D*>(this)->read_impl(buf, len);
}
};
struct TempSensor : SensorBase<TempSensor> {
int read_impl(uint8_t* buf, uint16_t len){
buf[0] = raw_temp >> 8; buf[1] = raw_temp & 0xFF;
return (len >= 2) ? 2 : 0;
}
uint16_t raw_temp = 0;
};
多传感器同接口、不同实现,编译期生成各自代码,调用可被内联;相比继承+虚函数省vtable指针,相比dynamic_cast彻底不依赖typeinfo。 若必须运行时分发,用函数指针表或枚举state,比虚继承更可控。
四、禁动态分配,用静态容器
std::vector/std::string会引入堆分配与异常路径,小MCU尽量不用。固定容量用std::array,字符串用char[]+snprintf,复杂表用ETL的etl::vector/etl::string或自写池:
alignas(uint32_t) static uint8_t pool[512];
inline void* operator new(size_t n, void* p) noexcept { (void)n; return p; }
inline void operator delete(void*, void*) noexcept {}
struct LogBuf {
char data[128]; uint16_t len = 0;
void append(const char* s){
while(*s && len < sizeof(data)-1) data[len++] = *s++;
data[len]=0;
}
};
全局重载operator new为delete,可让意外写new直接链接受阻;只保留placement new构造静态对象,避免碎片和非确定时延。
五、验收与边界
构建后用size看.text/.data/.bss,用nm --size-sort筛大头:
arm-none-eabi-size firmware.elf
arm-none-eabi-nm --size-sort firmware.elf | tail -20
对比基准:开异常+RTTI、关异常RTTI、再加LTO+gc-sections三档。异常重度工程(多try、深调用、STL异常处理)关掉后.eh_frame/.gcc_except_table可省不少;RTTI仅在有大量多态类时省得多,纯模板工程本来就几乎不靠它。 注意:关异常后第三方C++库若内部throw,要换无异常移植版或自己包错误码;构造函数不做阻塞硬件操作,全局对象构造顺序用函数内static局部替代,降低启动依赖。
按“禁异常RTTI+禁隐式堆+CRTP/错误码+sections/LTO/gc”这套收口,8~64KB级Flash的Cortex-M工程能把C++写得接近等价C的体积,同时保留类型安全、模板复用和RAII式资源管理。





