结论前置:RT-Thread/MCU 上可以安全使用 C++,但必须满足三个前提:
- 强制编译开关
-fno-exceptions -fno-rtti- 禁止运行时堆分配,用固定大小容器替代 STL 动态容器
- 运行时多态改为编译期多态(模板 / CRTP / 函数指针表)
满足这三点后,C++ 带来的零开销抽象、类型安全、RAII 资源管理,比 C 有实质性优势。
1. 为什么在 MCU 上引入 C++
嵌入式 C 语言面对复杂业务时的痛点很具体:硬件抽象靠 void* + 函数指针手工组装,没有类型检查;资源管理靠约定,稍不注意就泄漏;相似逻辑因类型不同复制多份,测试覆盖困难。
C++ 解决这些问题的机制大多是编译期的,不依赖运行时:模板在编译期展开,constexpr 在编译期计算,RAII 析构函数在作用域结束时自动调用。这些特性的运行时开销为零,与 C 代码生成的机器码等价。
引入代价的是运行时特性:异常(.eh_frame 段、栈展开)、RTTI(typeid、dynamic_cast、类型信息表)、虚函数(vtable 指针 + 间接调用)。MCU 固件关掉前两项、限制第三项,C++ 的收益就是纯赚。
规范依据:
- MISRA C++ 2023:禁止
dynamic_cast(Rule 21.3.1),性能关键路径限制虚函数,Advisory Rule 建议编译期分发优先。 - AUTOSAR C++14:禁用异常(
-fno-exceptions),禁用 RTTI(-fno-rtti),关键任务线程禁止动态内存分配。
2. RT-Thread C++ 支持现状(v5.1.0)
RT-Thread v5.1.0 通过 components/libc/cplusplus 组件提供 C++ 支持,分两层:
2.1 Kconfig 配置项
RT-Thread kernel → C++ support
├── RT_USING_CPLUSPLUS # 总开关
├── RT_USING_CPLUSPLUS11 # 启用 C++11 标准多线程特性
├── RT_USING_CPP_WRAPPER # RT-Thread API 的 C++ 封装
└── RT_USING_CPP_EXCEPTIONS # 异常(不推荐,增加 ROM 开销)
启用 RT_USING_CPLUSPLUS11 时自动依赖 RT_USING_PTHREADS,底层通过 POSIX pthread 实现标准线程原语。
2.2 C++ 标准支持范围
| 特性 | 支持情况 | 备注 |
|---|---|---|
C++11 std::thread / std::mutex / std::condition_variable | 完整支持 | 基于 RT-Thread pthread 实现 |
C++11 std::future / std::atomic | 完整支持 | atomic_8.c 专门适配 |
| C++14 语言特性 | 支持 | CMake 默认 CMAKE_CXX_STANDARD 14 |
C++17 语言特性(if constexpr、结构化绑定等) | 工具链支持 | arm-none-eabi-g++ 10+ |
C++17 标准库(std::filesystem 等) | 不适用 | 无 OS 文件系统抽象 |
| 异常 | 可选但不推荐 | 增加 ROM 5-15%,官方明确警告 |
| RTTI | 不支持 | 官方明确禁用 |
| 静态类变量全局构造 | 强烈不推荐 | 构造时刻不可控,易引发启动顺序问题 |
实践建议:语言特性用 C++17(if constexpr、std::enable_if_t、fold expression 等),标准库仅用 C++11 已验证的并发原语和 <algorithm> / <array> / <atomic>,不依赖需要动态分配的容器。
3. 编译器基线与配置
3.1 必须加的编译开关
target_compile_options(<target> PRIVATE
-std=c++17
-fno-exceptions # 消除 .eh_frame/.gcc_except_table,节省 ROM 5-15%
-fno-rtti # 消除 typeinfo 对象,节省 ROM 1-3%
-ffunction-sections -fdata-sections # 配合 --gc-sections 裁剪未使用代码
)
target_link_options(<target> PRIVATE --gc-sections)
| 开关 | ROM 节省 | 说明 |
|---|---|---|
-fno-exceptions | ~8 KB | 消除栈展开表和 __cxa_ 符号 |
-fno-rtti | ~2 KB | 消除每个多态类的 typeinfo 对象 |
--gc-sections | 可达 20%+ | 裁剪未被引用的函数/数据段 |
加上这三项后,C++ 固件体积通常与等价 C 实现差距在 3% 以内。
3.2 RT-Thread SCons 工程配置
# rtconfig.py
CXXFLAGS = CFLAGS + ' -std=c++17 -fno-exceptions -fno-rtti'
RT-Thread 的 RT_USING_CPP_EXCEPTIONS 配置项若未启用,构建系统会自动追加 -fno-exceptions;但显式写在 rtconfig.py 中更可靠,避免第三方库覆盖。
4. 多态策略:编译期 vs 运行时
4.1 虚函数的真实开销
含虚函数的类,编译器在对象头部插入 vtable 指针(Cortex-M:4 字节)。每次虚调用:
LDR r0, [obj] ; 取 vtable 指针(1 次内存读)
LDR r1, [r0, #offset] ; 取函数指针(1 次内存读)
BLX r1 ; 间接跳转(分支预测失效)
与直接调用相比,多 2 次内存读 + 间接跳转,Cortex-M4 无缓存下热路径增加约 3-5 cycles,且无法内联,编译器优化截断。
4.2 C 语言 OOP:const 函数指针表
RT-Thread 设备驱动框架(rt_device_ops)就采用此模式,函数表放 .rodata,零运行时初始化:
typedef struct {
rt_err_t (*init) (rt_device_t dev);
rt_err_t (*open) (rt_device_t dev, rt_uint16_t oflag);
rt_size_t (*read) (rt_device_t dev, rt_off_t pos,
void *buf, rt_size_t size);
rt_size_t (*write)(rt_device_t dev, rt_off_t pos,
const void *buf, rt_size_t size);
rt_err_t (*close) (rt_device_t dev);
} struct rt_device_ops;
static const struct rt_device_ops uart_ops = {
.init = uart_init,
.open = uart_open,
.read = uart_read,
.write = uart_write,
.close = uart_close,
};
无 vtable 指针注入对象头,sizeof(struct) 不变,调用开销与虚函数相当,但可以静态分析和内联。
4.3 C++ 编译期多态:CRTP
需要 C++ 类型系统保护时,CRTP 是零开销替代虚函数的标准方案:
template <typename Derived>
class SensorBase {
public:
void read() {
/* 编译期决议,O2 下全部内联,机器码与手写 C 等价 */
static_cast<Derived*>(this)->read_impl();
}
};
class TemperatureSensor : public SensorBase<TemperatureSensor> {
public:
void read_impl() { /* 读 ADC 寄存器 */ }
};
4.4 选择矩阵
| 场景 | 推荐方案 | 理由 |
|---|---|---|
| 后端在编译期确定 | 模板特化 / CRTP | 零运行时开销,可内联 |
| 后端在运行期配置 | const 函数指针表 | O(1) 分发,无 vtable 注入 |
| 性能关键热路径 | 禁止虚函数 | MISRA C++ Advisory |
| 跨模块插件(MCU 不适用) | 虚函数 | 唯一合理场景 |
5. 模板与零开销抽象
5.1 enable_if 类型分发:编译期选择实现
同一函数名,浮点和整数走不同实现路径,编译器在实例化时决议,运行时无任何分支:
class Utils {
public:
/* 浮点版本:误差比较 */
template <typename T,
std::enable_if_t<std::is_floating_point<T>::value, bool> = true>
static bool is_equal(T a, T b) noexcept {
return std::abs(a - b) < static_cast<T>(1e-6);
}
/* 整数版本:精确比较 */
template <typename T,
std::enable_if_t<std::is_integral<T>::value, bool> = true>
static bool is_equal(T a, T b) noexcept {
return a == b;
}
/* 类型安全的多参数乘法,common_type 自动提升精度 */
template <typename Out, typename First, typename... Rest>
static constexpr Out static_mul(First f, Rest... r) noexcept {
using C = std::common_type_t<First, Rest...>;
return static_cast<Out>(static_cast<C>(f) * static_mul<C>(r...));
}
template <typename Out, typename A, typename B>
static constexpr Out static_mul(A a, B b) noexcept {
using C = std::common_type_t<A, B>;
return static_cast<Out>(static_cast<C>(a) * static_cast<C>(b));
}
};
/* 调用:编译期展开为一条乘法链,static_cast 保证类型安全 */
uint32_t pixels = Utils::static_mul<uint32_t>(width, height, channels);
5.2 constexpr:编译期生成查找表
/* CRC32(反转多项式 0xEDB88320)查找表,256 项,放入 .rodata */
constexpr uint32_t crc32_byte(uint32_t i) noexcept {
for (int b = 0; b < 8; ++b) {
i = (i & 1U) ? (0xEDB88320U ^ (i >> 1)) : (i >> 1);
}
return i;
}
template <size_t... I>
constexpr std::array<uint32_t, 256> make_crc32_table(
std::index_sequence<I...>) noexcept {
return {{ crc32_byte(static_cast<uint32_t>(I))... }};
}
constexpr auto kCrc32Table =
make_crc32_table(std::make_index_sequence<256>{});
/* 运行时查表:仅一条 LDR,无计算 */
uint32_t crc32_update(uint32_t crc, uint8_t byte) noexcept {
return (crc >> 8) ^ kCrc32Table[(crc ^ byte) & 0xFFU];
}
5.3 注意:代码体积膨胀
模板每个实例化参数组合生成独立机器码。控制方式:
- 公共逻辑提取到非模板基类(类型擦除)
- 用
extern template显式实例化,抑制重复生成 - 定期用
arm-none-eabi-size --format=sysv检查各段大小
6. 固定大小容器:替代 STL 动态容器
MCU 固件禁止堆分配,std::string / std::vector 直接排除。替代方案是栈内联、编译期容量固定的容器。newosp 的 vocabulary.hpp 提供了经过生产验证的实现,移植自 eclipse-iceoryx。
6.1 FixedString<N>
template <uint32_t Capacity>
class FixedString {
static_assert(Capacity > 0U, "Capacity must be > 0");
public:
constexpr FixedString() noexcept : buf_{'\0'}, size_(0U) {}
/* 字符串字面量构造:编译期长度检查,超长直接报错 */
template <uint32_t N, typename = std::enable_if_t<(N <= Capacity + 1U)>>
FixedString(const char (&str)[N]) noexcept : size_(N - 1U) {
static_assert(N - 1U <= Capacity, "literal exceeds FixedString capacity");
std::memcpy(buf_, str, N);
}
/* 运行时截断构造(显式标签,避免静默截断) */
FixedString(TruncateToCapacity_t, const char* str) noexcept;
[[nodiscard]] constexpr const char* c_str() const noexcept { return buf_; }
[[nodiscard]] constexpr uint32_t size() const noexcept { return size_; }
static constexpr uint32_t capacity() noexcept { return Capacity; }
[[nodiscard]] constexpr bool empty() const noexcept { return size_ == 0U; }
private:
char buf_[Capacity + 1U];
uint32_t size_;
};
/* 使用 */
FixedString<64> path = "firmware/default.bin"; /* 编译期长度检查 */
FixedString<64> name{TruncateToCapacity, user_buf}; /* 显式截断,安全 */
| 维度 | FixedString<N> | std::string |
|---|---|---|
| 内存 | 栈,N+5 字节 | 堆,24 B 头 + 堆内容 |
| 分配 | 零(栈帧) | malloc,可能碎片 |
| 编译期长度检查 | 是(static_assert) | 否 |
| MCU 适用 | 是 | 否(禁堆场景) |
6.2 FixedVector<T, N> 配合 STL 算法
FixedVector<uint16_t, 256> adc_samples;
adc_samples.push_back(read_adc());
/* std::sort / std::transform 不分配堆,完全兼容 */
std::sort(adc_samples.begin(), adc_samples.end());
auto peak = *std::max_element(adc_samples.begin(), adc_samples.end());
---
## 7. 内存池 + 智能指针:零堆 RAII
当对象生命周期无法绑定到固定栈作用域时,用内存池 + 自定义删除器替代裸 `new/delete`。[newosp](https://github.com/DeguiLiu/newosp) 的 `mem_pool.hpp` 提供了嵌入式友好的固定块内存池实现。
### 7.1 固定块内存池(嵌入式空闲链表)
```cpp
template <uint32_t BlockSize, uint32_t MaxBlocks>
class FixedPool {
static_assert(BlockSize >= sizeof(uint32_t), "BlockSize >= 4");
static_assert(MaxBlocks > 0U, "MaxBlocks must be > 0");
public:
FixedPool() : free_head_(0U), used_count_(0U) {
/* 构造时建立嵌入式空闲链表:block[i].next = i+1 */
for (uint32_t i = 0U; i < MaxBlocks - 1U; ++i) store_index(i, i + 1U);
store_index(MaxBlocks - 1U, kInvalid);
}
void* allocate() noexcept; /* O(1),从链表头取块 */
void free(void* ptr) noexcept; /* O(1),归还链表头 */
bool owns(const void* ptr) const noexcept; /* 越界检测 */
private:
alignas(uint32_t) uint8_t storage_[BlockSize * MaxBlocks];
uint32_t free_head_;
uint32_t used_count_;
mutable std::mutex mutex_;
static constexpr uint32_t kInvalid = UINT32_MAX;
};
关键设计:空闲块复用自身内存存储 next 指针(embedded free list),storage_ 静态分配在 .bss,零运行时堆依赖。
7.2 unique_ptr + 自定义删除器
static FixedPool<sizeof(FrameTask), 8> s_frame_pool;
struct FramePoolDeleter {
void operator()(FrameTask* p) const noexcept {
if (p != nullptr) {
p->~FrameTask(); /* 显式析构 */
s_frame_pool.free(p); /* 归还内存块 */
}
}
};
/* 分配并构造 */
void* mem = s_frame_pool.allocate();
if (mem == nullptr) { return -RT_ENOMEM; }
auto task = std::unique_ptr<FrameTask, FramePoolDeleter>(
new (mem) FrameTask(param) /* placement new,零堆 */
);
task->process();
/* 作用域结束:FramePoolDeleter → ~FrameTask() + pool.free() */
7.3 shared_ptr 的限制
std::shared_ptr 的控制块(引用计数 + 删除器)本身需要堆分配。严格禁堆场景不可用。替代方案:
- 侵入式引用计数(对象自身含
refcount字段,boost::intrusive_ptr风格) - 设计上改为单一所有者(
unique_ptr+ 移动语义)
8. 无锁 SPSC Ring Buffer:中断与主循环数据传递
中断与主循环之间的数据传递是 MCU 最高频的"并发"场景。newosp 的 spsc_ringbuffer.hpp 提供了 wait-free 实现,并针对单核 MCU 做了 FakeTSO 优化。
8.1 模板声明
template <typename T,
size_t BufferSize = 16, /* 必须是 2 的幂 */
bool FakeTSO = false, /* 单核 MCU 设为 true */
typename IndexT = size_t>
class SpscRingbuffer {
static_assert((BufferSize & (BufferSize - 1)) == 0, "must be power of 2");
/* 线程安全约束:
* 生产者(Push/PushBatch):仅一个线程/中断
* 消费者(Pop/PopBatch) :仅一个线程
* 两者不得互换角色 */
};
8.2 FakeTSO:单核 MCU 的编译器屏障优化
多核 ARM(Cortex-A)需要 memory_order_acquire/release 生成 DMB 屏障。单核 Cortex-M 上,中断打断主循环时主循环停止,CPU 可见性由硬件保证,不需要 DMB,只需防止编译器乱序:
/* FakeTSO = true 时的 Push:relaxed 原子 + 编译器信号屏障 */
bool Push(const T& val) noexcept {
const IndexT head = head_.load(std::memory_order_relaxed);
if (full(head, tail_.load(std::memory_order_relaxed))) return false;
buf_[head & kMask] = val;
/* atomic_signal_fence:仅阻止编译器乱序,无 DMB 指令生成 */
std::atomic_signal_fence(std::memory_order_acq_rel);
head_.store(next(head), std::memory_order_relaxed);
return true;
}
if constexpr 在编译期选择 FakeTSO / 标准路径,两个版本无运行时开销差异。
8.3 批量操作减少原子操作次数
/* 逐个 Push N 个元素:2N 次原子操作 */
/* PushBatch N 个元素:2 次原子操作(一次读 head,一次写 head) */
void ADC_IRQHandler() {
uint16_t buf[8];
HAL_ADC_GetValues(buf, 8);
g_adc_ring.PushBatch(buf, 8); /* 高频中断(>10 kHz)下优势显著 */
}
---
## 9. 错误处理与 C/C++ 互操作
### 9.1 `expected<V, E>`:无异常的链式错误处理
`-fno-exceptions` 后构造函数不能抛异常,深层错误靠返回码传递。`expected<V, E>` 比裸返回码更清晰,比 `std::optional` 多携带错误信息,C++23 已纳入标准,C++17 可自行实现:
```cpp
template <typename V, typename E>
class expected {
public:
static expected ok(V v) { return expected(std::move(v), true); }
static expected err(E e) { return expected(std::move(e), false); }
bool has_value() const noexcept { return ok_; }
const V& value() const noexcept { return val_; }
const E& error() const noexcept { return err_; }
/* 链式操作:有值则映射,否则透传错误 */
template <typename F>
auto and_then(F&& f) -> decltype(f(std::declval<V>())) {
return ok_ ? f(val_) : decltype(f(std::declval<V>()))::err(err_);
}
private:
union { V val_; E err_; };
bool ok_;
};
/* 使用示例:分配 + 构造,错误透传 */
enum class PoolErr { Exhausted };
expected<uint8_t*, PoolErr> alloc_frame_buf() {
void* mem = s_frame_pool.allocate();
if (mem == nullptr) return expected<uint8_t*, PoolErr>::err(PoolErr::Exhausted);
return expected<uint8_t*, PoolErr>::ok(static_cast<uint8_t*>(mem));
}
auto result = alloc_frame_buf();
if (!result.has_value()) {
rt_kprintf("[WARN] frame pool exhausted\n");
return -RT_ENOMEM;
}
9.2 C/C++ 互操作:extern "C" 规范
RT-Thread 内核是 C 实现,C++ 组件通过 extern "C" 与之交互,extern "C" 保证符号名不被 mangle:
/* 统计库头文件:C/C++ 双模式兼容 */
#ifdef __cplusplus
extern "C" {
#endif
typedef long double DoubleType;
typedef struct {
DoubleType sum; /* Σx */
DoubleType sum_sq; /* Σx² */
DoubleType pre; /* 前一个样本(用于计算 delta)*/
DoubleType max;
DoubleType min;
DoubleType delta_max;
uint64_t count;
} SimpleMean;
void mean_reset (SimpleMean *out);
void mean_add (SimpleMean *out, DoubleType x);
DoubleType mean_mean (const SimpleMean *in);
/* 方差公式:σ² = E[X²] - (E[X])² = sum_sq/n - (sum/n)²
* 使用 long double 规避大数相减精度损失 */
DoubleType mean_std_dev(const SimpleMean *in);
#ifdef __cplusplus
}
#endif
extern "C" 块内不能使用 C++ 特有语法(引用、默认参数、模板)。C 结构体在 C++ 中可直接使用;若需要成员函数,用 wrapper class 封装,不修改 C 接口。
RT-Thread 的 rt_thread_create、rt_malloc 等内核 API 已在头文件中加了 extern "C",C++ 代码直接调用无需额外处理。
9.3 STL 算法:哪些能用
<algorithm> 纯模板算法操作调用者提供的迭代器区间,不自行分配堆内存,可用于 std::array / FixedVector:
/* 推荐使用 */
std::sort(v.begin(), v.end());
std::find_if(v.begin(), v.end(), pred);
std::transform(a.begin(), a.end(), b.begin(), fn);
std::any_of / std::all_of / std::count_if / std::fill / std::copy
禁止使用(可能申请临时堆缓冲):
| 算法 | 原因 |
|---|---|
std::stable_sort | 最坏情况申请 O(N) 临时空间 |
std::inplace_merge | 有时申请 O(N) 临时空间 |
std::regex_* | 大量堆分配 |
10. 总结:RT-Thread C++ 决策矩阵
| C++ 特性 | MCU/RT-Thread 适用性 | 替代 / 限制 |
|---|---|---|
| 类与封装 | 推荐 | — |
| 继承(非虚) | 推荐 | — |
| 虚函数(热路径) | 禁止 | CRTP / const 函数指针表 |
| 异常 | 禁止 | expected<V,E> / 返回码 |
RTTI / dynamic_cast | 禁止 | 编译期 type_traits |
模板 / constexpr | 推荐 | 控制实例化组合数量 |
std::string / std::vector | 禁止 | FixedString<N> / FixedVector<T,N> |
std::array | 推荐 | 完全栈分配 |
std::unique_ptr | 推荐 | 搭配内存池自定义删除器 |
std::shared_ptr | 禁止(严格禁堆) | 侵入式引用计数 / unique_ptr |
std::thread / std::mutex | 推荐(RT-Thread v5 已验证) | 需启用 RT_USING_CPLUSPLUS11 |
std::atomic | 推荐 | 单核 MCU 用 FakeTSO + atomic_signal_fence |
<algorithm> 查询 / 变换 | 推荐 | 避免 stable_sort / inplace_merge |
| 命名空间 | 推荐 | 头文件禁止 using namespace |
| 静态类变量全局构造 | 禁止 | 构造时刻不可控,用函数级静态或显式初始化 |
三个前提满足后,C++ 在 RT-Thread/MCU 固件中的综合工程质量(类型安全、RAII、零开销抽象、可测试性)明显优于 C,是值得投入的技术选择。
参考资料
- RT-Thread C++ 组件文档
- newosp —— 嵌入式 C++17 框架(FixedString / FixedPool / SpscRingbuffer / expected)
- eclipse-iceoryx —— 零拷贝 IPC 中间件,FixedString 原型
- MISRA C++ 2023
- AUTOSAR C++14 Coding Guidelines