结论前置:RT-Thread/MCU 上可以安全使用 C++,但必须满足三个前提:

  1. 强制编译开关 -fno-exceptions -fno-rtti
  2. 禁止运行时堆分配,用固定大小容器替代 STL 动态容器
  3. 运行时多态改为编译期多态(模板 / CRTP / 函数指针表)

满足这三点后,C++ 带来的零开销抽象、类型安全、RAII 资源管理,比 C 有实质性优势。

1. 为什么在 MCU 上引入 C++

嵌入式 C 语言面对复杂业务时的痛点很具体:硬件抽象靠 void* + 函数指针手工组装,没有类型检查;资源管理靠约定,稍不注意就泄漏;相似逻辑因类型不同复制多份,测试覆盖困难。

C++ 解决这些问题的机制大多是编译期的,不依赖运行时:模板在编译期展开,constexpr 在编译期计算,RAII 析构函数在作用域结束时自动调用。这些特性的运行时开销为零,与 C 代码生成的机器码等价。

引入代价的是运行时特性:异常(.eh_frame 段、栈展开)、RTTI(typeiddynamic_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 constexprstd::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 直接排除。替代方案是栈内联、编译期容量固定的容器。newospvocabulary.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 最高频的"并发"场景。newospspsc_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_creatert_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,是值得投入的技术选择。


参考资料