源码仓库: newosp | 本文代码引用基于 newosp v0.4.3 (43 headers, 1153 tests)
筛选标准: 只保留 C11 在语言层面无法实现的能力。 边界检查、SBO 回调、ARM 内存序、cache line 对齐、
-fno-exceptions等 C 均可做到,已移除。
姊妹篇: [C11 做不到的事: 10 项 C++17 语言级不可替代能力]({{< ref "cpp17_what_c_cannot_do" >}}) -- 语言层面的系统性对比与完整代码示例。
1. 编译期类型成员校验 -- bus.hpp
C 的 void* 不携带类型信息,编译器无法验证传入类型是否属于合法集合。
include/osp/bus.hpp:376-385:
template <typename T, typename... Types>
struct VariantIndex<T, std::variant<Types...>> {
static constexpr size_t value =
detail::VariantIndexImpl<T, 0, std::variant<Types...>>::value;
static_assert(value != static_cast<size_t>(-1),
"Type not found in PayloadVariant");
};
include/osp/bus.hpp:500-505 -- Subscribe 调用点验证:
template <typename T, typename Func>
SubscriptionHandle Subscribe(Func&& func) noexcept {
constexpr size_t type_idx = VariantIndex<T, PayloadVariant>::value;
static_assert(type_idx < OSP_BUS_MAX_MESSAGE_TYPES,
"Type index exceeds OSP_BUS_MAX_MESSAGE_TYPES");
工程决策: VariantIndex 在模板实例化时递归展开,将"类型是否在合法集合中"从运行时 tag 校验提升为编译期硬错误。C 的 subscribe(bus, GPS_TAG, handler) 中 tag 写错不会产生编译错误。
2. 穷举式类型分发 -- bus.hpp
C 的 switch (msg->tag) 缺少 case 只产生 -Wswitch 警告。
include/osp/bus.hpp:84-90:
template <class... Ts>
struct overloaded : Ts... {
using Ts::operator()...;
};
template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
工程决策: overloaded + std::visit 的组合使新增消息类型时,所有未更新的处理点编译失败而非运行时丢消息。对于工业嵌入式系统,"编译不过"远好于"部署后丢数据"。
3. 强类型别名 -- vocabulary.hpp
C 的 typedef uint32_t TimerId 和 typedef uint32_t NodeId 是同一类型。
include/osp/vocabulary.hpp:739-763:
template <typename T, typename Tag>
class NewType final {
public:
constexpr explicit NewType(T val) noexcept : val_(val) {}
constexpr T value() const noexcept { return val_; }
constexpr bool operator==(NewType rhs) const noexcept { return val_ == rhs.val_; }
constexpr bool operator!=(NewType rhs) const noexcept { return val_ != rhs.val_; }
private:
T val_;
};
using TimerTaskId = NewType<uint32_t, TimerTaskIdTag>;
using SessionId = NewType<uint32_t, SessionIdTag>;
工程决策: TimerTaskId id; SessionId sid = id; 编译失败。在 newosp 中,节点 ID、定时器 ID、会话 ID 均使用 NewType 包装。零运行时开销 -- sizeof(NewType<uint32_t, Tag>) == sizeof(uint32_t),编译器直接传寄存器。
4. if constexpr -- spsc_ringbuffer.hpp / config.hpp / fault_collector.hpp
C 的 #ifdef 只能基于宏开关,无法检测类型属性。
(a) include/osp/spsc_ringbuffer.hpp:144-157 -- 根据 T 是否 trivially copyable 选择路径:
if constexpr (kTriviallyCopyable) {
std::memcpy(&data_buff_[head_offset], buf + written, first_part * sizeof(T));
} else {
for (size_t i = 0; i < to_write; ++i) {
data_buff_[(head_offset + i) & kMask] = buf[written + i];
}
}
同一文件 184 行 (Pop)、210 行 (PopBatch) 使用相同模式。
(b) include/osp/config.hpp:534-560 -- 编译期递归展开多后端分派:
template <typename First, typename... Rest>
expected<void, ConfigError> DispatchFile(const char* path, ConfigFormat format) {
if (First::kFormat == format)
return ConfigParser<First>::ParseFile(*this, path);
if constexpr (sizeof...(Rest) > 0)
return DispatchFile<Rest...>(path, format);
return expected<void, ConfigError>::error(ConfigError::kFormatNotSupported);
}
(c) include/osp/fault_collector.hpp:580 -- 根据回调返回类型选择控制流:
if constexpr (std::is_same_v<decltype(fn(recent_ring_[idx])), bool>) {
if (!fn(recent_ring_[idx])) { break; }
} else {
fn(recent_ring_[idx]);
}
工程决策: 三处 if constexpr 的共同特征 -- 在同一个函数模板中,根据类型属性生成不同代码路径,编译后只保留命中分支。C 的 #ifdef 无法区分 SensorData 是否 trivially copyable,必须由程序员手动选择拷贝方式。
5. constexpr 函数 -- bus.hpp / app.hpp
C 的 const 不是编译期常量合同。C 没有"函数必须在编译期求值"的语言机制。
include/osp/bus.hpp:70-78:
constexpr uint32_t Fnv1a32(const char* str) noexcept {
if (str == nullptr) return 0;
uint32_t hash = 2166136261u;
while (*str) {
hash ^= static_cast<uint32_t>(*str++);
hash *= 16777619u;
}
return hash;
}
include/osp/app.hpp:80:
constexpr uint32_t MakeIID(uint16_t app_id, uint16_t ins_id) noexcept {
return (static_cast<uint32_t>(app_id) << 16) | static_cast<uint32_t>(ins_id);
}
工程决策: Fnv1a32 用于 topic 路由,编译期将字符串 "sensor/imu" 折叠为立即数,运行时零开销。MakeIID 将 app_id/ins_id 编码为 32 位实例标识符,同样在编译期完成。C 可以用宏做 MAKE_IID,但无法在宏中写 while 循环实现哈希函数。
6. 模板实例化 -- spsc_ringbuffer.hpp / bus.hpp
C 的 void* + size_t 传参让编译器丢失常量信息,index % depth 变成运行时除法。
include/osp/spsc_ringbuffer.hpp:74-86:
template <typename T, size_t BufferSize = 16, bool FakeTSO = false, typename IndexT = size_t>
class SpscRingbuffer {
static_assert(BufferSize != 0, "Buffer size cannot be zero.");
static_assert((BufferSize & (BufferSize - 1)) == 0, "Buffer size must be a power of 2.");
static_assert(sizeof(IndexT) <= sizeof(size_t), "Index type size must not exceed size_t.");
static_assert(std::is_unsigned<IndexT>::value, "Index type must be unsigned.");
include/osp/bus.hpp:406-420:
static constexpr uint32_t kQueueDepth = static_cast<uint32_t>(OSP_BUS_QUEUE_DEPTH);
static constexpr uint32_t kBufferMask = kQueueDepth - 1;
static_assert((kQueueDepth & (kQueueDepth - 1)) == 0,
"Queue depth must be power of 2");
工程决策: SpscRingbuffer<SensorData, 256> 和 SpscRingbuffer<MotorCmd, 64> 是不同类型,编译器为每个实例化独立优化 -- & (256-1) 编译为单条 AND 立即数指令。C11 的 _Static_assert 只能断言整型常量表达式,无法断言类型属性 (is_unsigned、is_trivially_copyable)。
7. RAII -- vocabulary.hpp / node.hpp / shm_transport.hpp
标准 C 没有析构函数。goto cleanup 是手动操作,漏一条路径就泄漏。
include/osp/vocabulary.hpp:774-801:
class ScopeGuard final {
public:
explicit ScopeGuard(FixedFunction<void()> cleanup) noexcept
: cleanup_(static_cast<FixedFunction<void()>&&>(cleanup)), active_(true) {}
~ScopeGuard() {
if (active_ && cleanup_) { cleanup_(); }
}
void release() noexcept { active_ = false; }
};
include/osp/node.hpp:177 -- Node 析构自动清理全部订阅:
~Node() noexcept { Stop(); }
include/osp/shm_transport.hpp:98 -- SharedMemorySegment 析构自动 munmap + shm_unlink。
工程决策: newosp 中 RAII 的三层应用 -- (1) ScopeGuard 用于临时资源的确定性清理; (2) Node 析构自动取消所有订阅,防止悬空回调; (3) SharedMemorySegment 析构自动释放共享内存。编译器保证 return、异常、作用域结束等所有退出路径均调用析构函数。
8. Fold Expression + CRTP -- worker_pool.hpp / static_node.hpp
C 没有可变参数模板,也缺少零开销的编译期多态机制。
include/osp/worker_pool.hpp:519-522 -- 参数包自动展开:
template <typename... Types>
void SubscribeAllImpl(std::variant<Types...>* /*tag*/) noexcept {
(MaybeSubscribe<Types>(), ...);
}
include/osp/static_node.hpp -- CRTP 零 vtable 编译期多态:
template <typename Derived>
struct NodeBase {
void Process() {
static_cast<Derived*>(this)->DoProcess(); // 编译期解析,可内联
}
};
// DoProcess() 直接内联到调用点,零间接跳转
工程决策: Fold Expression 一行代码为 variant 中每个类型调用 MaybeSubscribe,C 需要手动枚举或 X-Macro 生成。CRTP 让 StaticNode 的 handler 在编译期绑定,避免 virtual 的间接调用开销和 vtable 内存占用 -- 在 ARM Cortex-A 上,消除一次虚函数调用可节省约 10-20 个周期 (cache miss 时更多)。
总结
| # | 能力 | C 的局限 | newosp 实现位置 |
|---|---|---|---|
| 1 | 编译期类型成员校验 | void* 无类型信息 | bus.hpp:376-385 |
| 2 | 穷举式类型分发 | switch 缺 case 仅警告 | bus.hpp:84-90 |
| 3 | 强类型别名 | typedef 是别名非新类型 | vocabulary.hpp:739-763 |
| 4 | 基于类型属性的分支消除 | 无 type trait,无 if constexpr | spsc_ringbuffer.hpp:144 |
| 5 | 保证编译期求值的函数 | const 非编译期合同 | bus.hpp:70-78 |
| 6 | 参数化专用代码生成 | void* + size_t 丢失常量 | spsc_ringbuffer.hpp:74-86 |
| 7 | 自动资源清理 | 无析构函数 | vocabulary.hpp:774-801 |
| 8 | 参数包展开 + CRTP | 无可变参数模板,函数指针不可内联 | worker_pool.hpp:519-522 |
这些能力并非孤立使用。以 AsyncBus 为例,一次 Publish 调用链涉及: 模板实例化生成专用队列代码 (第 6 项) -> VariantIndex 编译期校验消息类型 (第 1 项) -> constexpr 计算 topic hash (第 5 项) -> if constexpr 按类型选择拷贝策略 (第 4 项) -> RAII 保证 envelope 资源自动释放 (第 7 项)。五项能力在同一条热路径上协同工作,形成编译期到运行时的完整安全链。