我刚毕业做证券项目时,也曾按开发手册的“标准”操作,使用字符串黑板来传递数据。后来换了工作,在一次项目评审中被技术经理直接指出问题,也正是那次经历,让我学会了远比字符串黑板更高效、更优雅的数据传递方式。

我曾在一个数据高频更新的场景下做过测试,字符串黑板相比直接传递对象,性能差距接近10倍。这种因设计模式选择不当而带来的性能开销,是完全可以避免的。
一、用 std::any 替代 string

如果暂时无法对整个架构进行大规模重构,一个立竿见影的优化方法,就是将 unordered_map<string, string> 替换为 unordered_map<string, std::any>。
这是一个简单的黑板类实现示例:
class Blackboard {
public:
template<typename T>
void set(const std::string& key, T&& value) {
data_[key] = std::forward<T>(value);
}
template<typename T>
T& get(const std::string& key) {
return std::any_cast<T&&>(data_.at(key));
}
private:
std::unordered_map<std::string, std::any> data_;
};
现在,你可以直接存储结构体,而无需序列化为字符串:
struct Position { double x, y; };
bb.set("player_pos", Position{1.5, 2.3});
Position p = bb.get<Position>("player_pos"); // 类型安全,零解析
这种方式彻底避免了JSON解析、字符串拼接/拆分以及double与string之间转换带来的开销和精度损失。在我的测试中,std::any黑板相比字符串黑板性能提升了近10倍。
不过需要注意的是,std::any内部使用了类型擦除技术,小对象通常在栈上分配,大对象则会使用堆内存。如果你的键值集合是固定且已知的,比如只有Position、double、bool等少数几种类型,使用std::variant会获得更高的效率:
using Value = std::variant<Position, double, bool>;
std::unordered_map<std::string, Value> fixed_blackboard;
std::variant没有虚函数调用的开销,访问速度更快,但缺点在于灵活性较差。
二、用接口和依赖注入实现真解耦
真正的解耦,依赖于清晰的抽象边界。优秀的架构设计,不是将所有东西塞进一个“数据桶”,而是让每个组件只依赖其必需的最小接口,不多不少。

假设你的渲染组件需要获取玩家位置,一个更好的做法不是让它去黑板里查询“player_pos”,而是让它依赖一个定义清晰的 IPositionProvider 接口。这种面向接口的编程是软件设计模式的核心思想之一。
// 定义接口
class IPositionProvider {
public:
virtual ~IPositionProvider() = default;
virtual Position getPlayerPosition() const = 0;
};
// 渲染组件只依赖接口
class Renderer {
public:
explicit Renderer(IPositionProvider* provider)
: pos_provider_(provider) {}
void render() {
Position p = pos_provider_->getPlayerPosition();
// 直接使用,无需解析
std::cout << "Rendering at (" << p.x << ", " << p.y << ")\n";
}
private:
IPositionProvider* pos_provider_;
};
然后,由游戏的逻辑模块来实现这个接口:
class GameLogic : public IPositionProvider {
public:
Position getPlayerPosition() const override {
return player_pos_;
}
void update(double dt) {
player_pos_.x += dt; // 简单移动
}
private:
Position player_pos_{0.0, 0.0};
};
在主函数中进行组装和运行:
int main() {
GameLogic logic;
Renderer renderer(&logic);
for (int i = 0; i < 3; ++i) {
logic.update(0.1);
renderer.render();
}
return 0;
}
这种模式在大型游戏引擎和框架中被广泛使用。它的优势非常明显:编译时类型安全、零运行时解析开销、模块间低耦合高内聚、便于单元测试。虽然看起来比直接使用黑板模式多了几行代码,但它换来的是可维护性、可扩展性和运行时性能的全面胜利。
如果多个组件都需要位置信息,可以让它们都依赖同一个 IPositionProvider 实例。使用智能指针(如std::shared_ptr)来管理生命周期,确保数据提供者的生存期长于所有消费者。
三、用信号系统实现广播通信
在组件间数据传递时,我们常常会遇到一种场景:一个动作需要触发多个独立且互不相关的业务逻辑。

例如,在一个防伪追溯系统中,当终端扫描一个商品条码后,至少需要同时触发以下三个业务:
- 防窜货引擎判断商品是否跨区域销售。
- 合规性模块检查经销商资质是否过期。
- 消费者服务模块返回真伪验证结果。
如果让扫码模块直接去调用这三个服务,代码很快就会变得像“意大利面条”一样混乱不堪。更优雅的做法是,让扫码模块只负责发布“扫码事件”,谁关心这个事件,谁就自己来监听。
使用信号(Signal)/槽(Slot)机制是实现这种广播模式的最佳选择。下面是一个极简版的信号模板实现:
template<typename... Args>
class Signal {
std::vector<std::function<void(Args...)>> slots_;
public:
void connect(auto f){ slots_.push_back(f); }
void emit(Args... args){
for (auto& f : slots_) f(args...);
}
};
扫码服务定义并发射事件:
struct ScanEvent { /* product_id, dealer_id, ... */ };
Signal<ScanEvent> onScanned;
onScanned.emit(event);
各个监听模块各自独立地处理事件,彼此之间无需知晓对方的存在:
antiDiversion.onScan = [&](const ScanEvent& e) { /* 处理窜货逻辑 */ };
compliance.onScan = [&](const ScanEvent& e) { /* 检查资质逻辑 */ };
verifier.onScan = [&](const ScanEvent& e) { /* 验证真伪逻辑 */ };
onScanned.connect([&](auto e){ antiDiversion.onScan(e); });
// ... 连接其他监听器
这种发布-订阅模式在工业级软件,尤其是需要处理复杂事件流的系统中非常常见。
四、不要用反射来传递数据
反射(Reflection)能够在运行时查询类型信息、访问成员、调用函数,是一种强大的元编程工具。但请记住,它根本就不是为在进程内部传递数据而设计的。

反射的真正用武之地在于存档反序列化、编辑器工具链、脚本语言绑定等“工具性质”的任务。如果你在同进程通信中强行使用反射来读写数据,无异于“扛着大炮打蚊子”,不仅会让性能急剧下降(通常超过50%),还会让调试变得极其困难。对于追求性能的C++项目来说,这完全是得不偿失的。像 std::any 这类现代C++特性提供了更安全高效的运行时类型操作方案。
五、场景与方案选择速查表
没有一种方案是万能的,正确的选择取决于具体的应用场景。下表总结了不同场景下的推荐方案:
| 场景 |
推荐方案 |
性能 |
耦合度 |
适用阶段 |
| 快速优化现有黑板 |
std::any / variant 黑板 |
高(无序列化) |
中(仍依赖 key) |
立刻做 |
| 核心模块间数据流 |
接口 + 依赖注入 (DI) |
极高(直接调用) |
低(仅依赖抽象) |
长期重构 |
| 一对多事件通知 (UI/日志) |
信号/槽 |
中高(函数调用) |
极低(完全解耦) |
按需引入 |
| 需要运行时动态访问 |
RTTR 等反射库 |
低(元数据查找) |
低 |
仅限工具/脚本 |
六、总结:迈向现代C++架构
字符串不再是模块间通信的“万能胶水”。现代C++已经为我们提供了 std::any、纯虚接口、信号系统等一系列强大的工具,它们能在实现解耦的同时,最大限度地保持甚至提升运行时性能。

你可以立刻行动起来,将项目中那些 unordered_map<string, string> 替换成 unordered_map<string, std::any>,性能提升可能立竿见影。待系统稳定后,再将核心链路逐步迁移到基于接口的模式。这才是追求性能与优雅的C++项目应有的架构形态。
希望这些在实战中踩坑得来的经验,能帮助你避开性能陷阱。如果你也在项目中经历过字符串黑板带来的“折磨”,或是有其他高性能架构的设计心得,欢迎在云栈社区的C++板块与大家交流探讨。优化之路,始于对每一个细节的审慎思考。