☰
FreeCAD 中的 FastSignals:基于 C++17 的高性能信号槽库及其从 Boost.Signals2 的迁移实践
2026/10/17 2:33:06 网站建设 项目流程

FreeCAD 中的 FastSignals:基于 C++17 的高性能信号槽库及其从 Boost.Signals2 的迁移实践

【免费下载链接】FreeCADOfficial source code of FreeCAD, a free and opensource multiplatform 3D parametric modeler.项目地址: https://gitcode.com/GitHub_Trending/fr/FreeCAD

导读

FastSignals 是 FreeCAD 项目内置(src/3rdParty/FastSignals)的一套纯 C++17 实现的信号与槽(signals and slots)库,其公开 API 与 Boost.Signals2 兼容,可作为后者的即插即用替代品。本文将围绕该库的设计动机、核心 API、弱引用绑定(weak this)用法,以及从 Boost.Signals2 大规模迁移的完整步骤展开,并结合 FreeCAD 源码(如 src/App/Application.h 中大量fastsignals::signal成员)展示真实应用形态。读完后,你将掌握如何在 FreeCAD 中直接使用该库,并能独立完成存量代码的平滑迁移。

一、FastSignals 是什么

FastSignals 是一个与 Boost.Signals2 API 兼容的 C++17 信号/槽实现,由 iSpring 团队开源并以 MIT 协议发布(见 LICENSE)。正如 README.md 所述,它拥有三项核心承诺:

  • 作为 Boost.Signals2 的 drop-in 替代品:使用相同的 API,迁移成本低;
  • 更优的性能与更紧凑的二进制代码:这是其存在的根本理由;
  • 大部分操作线程安全:包括并发的连接(connect)、断开(disconnect)与发射(emit)。

它由紧凑的纯 C++17 代码实现,库结构集中在 libfastsignals 目录下,头文件位于libfastsignals/include/fastsignals/,实现文件位于libfastsignals/src/。

FreeCAD 在 3rdParty 构建体系中通过 src/3rdParty/CMakeLists.txt 中的add_subdirectory(FastSignals)引入该库,并在 App 核心层大量使用:以 src/App/Application.h 为例,文档对象的生命周期事件(新建、删除、重命名、保存、撤销/重做、属性变更等)均由fastsignals::signal承载,例如signalNewDocument、signalDeletedObject、signalBeforeChangeObject、signalChangedObject。这说明 FastSignals 并非孤立的三方代码,而是 FreeCAD 文档核心事件机制的实际基石。

二、为什么选择 FastSignals

why-fastsignals.md 明确列出了该库的取舍。

优势

  • 比 Boost.Signals2 更快;
  • 二进制代码更紧凑;
  • 与 Boost.Signals2 相同的 API。

局限与差异

  • 仅支持 C++17 兼容编译器:Visual Studio 2017、现代 Clang、现代 GCC;
  • 缺少少量 Boost.Signals2 中极少使用的特性:
    • 无法通过signal::connect_extended在槽内获得 connection;
    • 不支持slot::track的关联对象跟踪——请改用bind_weak(详见 bind_weak.md);
    • 没有shared_connection_block类的临时信号阻塞功能(注意:源码层面实际提供了shared_connection_block,见下文分析);
    • 由于没有disconnect(slot)重载,无法按槽等价断开;
    • 任何其他 API 差异都被视为 bug,欢迎上报。

基准测试结果

文档指出tests/libfastsignals_bench目录包含一个简单基准测试,对比 Boost.Signals2 与 libfastsignals 在信号被高频发射且分别挂载 0、1、8 个活跃连接时的性能,三种场景下 libfastsignals 快 3~6 倍。原文档给出了如下示例结果(相对耗时,数值越大代表相对 Boost 越快):

*** Results: measure emit_boost emit_fastsignals emit_boost/0 1.00 3.00 emit_boost/1 1.00 5.76 emit_boost/8 1.00 3.70 ***

需要说明的是,当前仓库中未包含该基准测试目录,上述为原文档记录的数据;如需复现,应结合自身编译环境实测。

源码层面的性能原理

从实现看,FastSignals 的性能优势有其结构基础:signal_impl.h 使用std::vector存储槽函数与 ID,配合自旋锁spin_mutex保护并发访问;signal.h 的operator()直接通过invoke<...>遍历槽并调用,而invoke内部使用if constexpr在编译期区分 void 与非 void 返回路径(见 signal_impl.h),非 void 时才构造 combiner 收集结果。这种“不为不使用的东西付费”的设计正是文档所强调的:唯一的例外是——你始终要为多线程支持付费(每次调用都涉及自旋锁)。

三、快速上手:基础示例

simple-examples.md 提供了两个可直接编译运行的最小示例。

3.1 signal 与 connection

// Creates signal and connects 1 slot, calls 2 times, disconnects, calls again. // Outputs: // 13 // 17 #include "libfastsignals/signal.h" using namespace fastsignals; int main() { signal<void(int)> valueChanged; connection conn; conn = valueChanged.connect([](int value) { cout << value << endl; }); valueChanged(13); valueChanged(17); conn.disconnect(); valueChanged(42); }

要点:connect返回connection对象,调用disconnect()后槽不再被调用,因此最后一次valueChanged(42)无任何输出。

3.2 scoped_connection 的 RAII 断开

// Creates signal and connects 1 slot, calls 2 times, calls again after scoped_connection destroyed. // - note: scoped_connection closes connection in destructor // Outputs: // 13 // 17 #include "libfastsignals/signal.h" using namespace fastsignals; int main() { signal<void(int)> valueChanged; { scoped_connection conn; conn = valueChanged.connect([](int value) { cout << value << endl; }); valueChanged(13); valueChanged(17); } valueChanged(42); }

与connection不同,scoped_connection在析构时自动断开连接(RAII 语义),因此离开作用域后valueChanged(42)同样不会触发任何槽。

3.3 源码中的连接类型体系

connection.h 完整定义了连接相关的类层次:

  • connection:管理信号-槽连接生命周期,connected()/disconnect()操作线程安全(可在其他线程发射期间断开);该类本身不线程安全,不能跨线程共享同一连接对象;
  • advanced_connection:支持临时阻塞回调执行,内部持有advanced_connection_impl(其m_blockCounter为std::atomic<int>);
  • shared_connection_block:用于阻塞 advanced connection 的回调执行;
  • scoped_connection/advanced_scoped_connection:析构时自动断开,可移动不可拷贝,scoped_connection提供release()释放连接。

注意一个细节:why-fastsignals.md声称 FastSignals 没有shared_connection_block,但从当前仓库源码看,connection.h 已提供该类的完整实现,且 signal.h 提供connect(slot, advanced_tag)重载来配合阻塞功能(仅限返回void的槽)。因此迁移时若用到临时阻塞,可参考该实现。

四、核心 API 深入:signal 类模板

signal.h 是库的门面,值得逐项理解:

template <class Signature, template <class T> class Combiner = optional_last_value> class signal;
  • Signature:形如Return(Arguments...)的函数签名,例如void(int)、void(const Document&, bool)(对应 FreeCAD 中signalNewDocument的声明方式);
  • Combiner:结果合并器,默认optional_last_value。其行为见 combiners.h:非 void 时把所有槽返回值缩减为“最后一个槽的返回值”,以std::optional<T>呈现;void特化则直接以void为result_type。

关键成员:

成员说明
connection connect(slot_type slot)订阅槽到信号,返回连接对象;每次调用信号(作为函数对象)时所有槽被依次调用
advanced_connection connect(slot_type slot, advanced_tag)带阻塞能力的连接,static_assert限定仅用于返回void的槽
void disconnect_all_slots()断开所有槽
std::size_t num_slots()返回已挂载槽数量
bool empty()无任何槽时返回 true
result_type operator()(args...)触发信号发射,调用所有槽
operator slot_type()允许信号自身作为另一信号的槽(信号链)

此外,signal禁止拷贝构造/拷贝赋值,但支持移动;std::swap通过 ADL 找到的自由函数swap实现(signal.h)。内部实现上,signal持有std::shared_ptr<detail::signal_impl>,因此移动语义安全且拷贝被有意禁止。

五、bind_weak:弱引用绑定的正确姿势

bind_weak.md 是 FastSignals 替代 Boost.Signals2track()/track_foreign()的官方方案。

5.1 基本用法

  • 使用fastsignals::bind_weak替代std::bind,确保绑定对象已销毁时方法不会被调用;
  • 第一个参数为指向 T 类成员函数的指针,第二个参数为shared_ptr<T>或weak_ptr<T>;
  • 示例:bind_weak(&Document::save(), document, std::placeholders::_1),其中document为weak_ptr<Document>或shared_ptr<Document>。

5.2 Weak this 惯用法

bind_weak实现了经典的 “weak this” 惯用法,用于避免异步/多线程程序中的悬垂指针与非法内存访问。原始手写形式如下:

struct Entity : std::enable_shared_from_this<Entity> { int value = 42; void print() { std::cout << "print called, num = " << value << std::endl; } std::function<void()> print_later() { // ! weak this idiom here ! auto weak_this = weak_from_this(); return [weak_this] { if (auto shared_this = weak_this.lock()) { shared_this->print(); } }; } }; int main() { auto entity = std::make_shared<Entity>(); auto print = entity->print_later(); // Prints OK. print(); // Prints nothing - last shared_ptr to the Entity destroyed, so `weak_this.lock()` will return nullptr. entity = nullptr; print(); }

5.3 用 bind_weak 规避接收者生命周期问题

将Entity::print()连接到信号后,在Entity销毁前后各发射一次:销毁后bind_weak无法lock()到对象,槽不会被调用,也不会发生内存访问违例——第二次event()静默无事。

#include <fastsignals/signal.h> #include <fastsignals/bind_weak.h> #include <iostream> using VoidSignal = fastsignals::signal<void()>; using VoidSlot = VoidSignal::slot_type; struct Entity : std::enable_shared_from_this<Entity> { int value = 42; VoidSlot get_print_slot() { // Here fastsignals::bind_weak() used instead of std::bind. return fastsignals::bind_weak(&Entity::print, weak_from_this()); } void print() { std::cout << "print called, num = " << value << std::endl; } }; int main() { VoidSignal event; auto entity = std::make_shared<Entity>(); event.connect(entity->get_print_slot()); // Here slot called - it prints `slot called, num = 42` event(); entity = nullptr; // Here nothing happens - no exception, no slot call. event(); }

5.4 底层实现

bind_weak.h 的实现以detail::weak_binder为核心:它保存成员函数指针与std::weak_ptr<ClassType>,重载operator()时先m_pObject.lock(),成功才调用成员函数,失败则返回ReturnType()的默认构造值。对外提供四个重载,覆盖 const/非 const 成员函数 ×shared_ptr/weak_ptr四种组合,最终通过std::bind包装返回可调用对象。

六、从 Boost.Signals2 迁移的完整指南

migration-from-boost-signals2.md 面向“大型代码库从 Boost.Signals2 迁移到 FastSignals”的场景,给出了处理已知迁移问题的正确路径。迁移中你大概率会遇到两类情况:

  • 代码直接使用boost::signals2::命名空间与<boost/signals2.hpp>头文件;
  • 代码间接依赖了<boost/signals2.hpp>隐式包含的第三方头文件。

迁移动机回顾

  • FastSignals不是 header-only,二进制代码更紧凑;
  • 使用 C++17 可变参数模板、constexpr if等现代元编程技术,编译更快、二进制更紧凑;
  • 对多数代码库可能更快——不为不使用的东西付费(多线程支持除外)。

Step 1:创建别名头文件

建议新建一个统一头文件,把命名空间与类型别名映射到 FastSignals,后续代码只需改动 include 路径即可。例如:

// signals.h #pragma once #include <fastsignals/signal.h> #include <fastsignals/bind_weak.h> namespace boost { namespace signals2 { // 将 Boost 命名空间映射到 fastsignals // using fastsignals::signal; ... } }

Step 2:重建并修复编译错误

2.1 补全缺失的 include

Boost.Signals2 是 header-only 库,会隐式包含大量 STL/Boost 头;FastSignals 不会。例如:

#include <boost/signals2.hpp> // Also includes std::map, boost::variant, boost::optional, etc. // Compiled OK even without `#include <map>`! std::map CreateMyMap();

迁移后这类代码必须显式包含对应头文件。下表给出了常见的缺失符号与应补的头文件:

ClassHeader
std::map#include <map>
boost::variant#include <boost/variant/variant.hpp>
boost::optional#include <boost/optional/optional.hpp>
boost::scoped_ptr#include <boost/scoped_ptr.hpp>
boost::noncopyable#include <boost/noncopyable.hpp>
boost::bind#include <boost/bind.hpp>
boost::function#include <boost/function.hpp>

如果只是想快速编译通过,可以在你的signals.h头中一次性补上这些 include;但官方明确警告:不推荐在 FastSignals 中引入这些额外头文件(见原文档代码注释)。

2.2 移除 void 信号槽中的多余 return

Boost.Signals2 允许如下代码(无警告),但 FastSignals 要求signal<void(...)>的槽不能返回非 void 值:

boost::signals2::signal<void()> event; event.connect([] { return true; });

修复方式:直接删除槽中的return,或用 lambda 包装槽并忽略其返回值。

2.3 用 bind_weak 替换 track() / track_foreign()

Boost.Signals2 可通过track(...)/track_foreign(...)跟踪关联对象的生命周期。原文档给出了典型迁移对照:左侧是 Boost 写法,右侧是 FastSignals 写法。

Boost.Signals2 写法:

#include <boost/signals2.hpp> #include <iostream> #include <memory> using VoidSignal = boost::signals2::signal<void()>; using VoidSlot = VoidSignal::slot_type; struct Entity : std::enable_shared_from_this<Entity> { int value = 42; VoidSlot get_print_slot() { // Here track() tracks object itself. return VoidSlot(std::bind(&Entity::print, this)).track_foreign(shared_from_this()); } void print() { std::cout << "print called, num = " << value << std::endl; } }; int main() { VoidSignal event; auto entity = std::make_shared<Entity>(); event.connect(entity->get_print_slot()); // Here slot called - it prints `print called, num = 42` event(); entity = nullptr; // This call does nothing. event(); }

FastSignals 写法:

#include <fastsignals/bind_weak.h> #include <iostream> using VoidSignal = fastsignals::signal<void()>; using VoidSlot = VoidSignal::slot_type; struct Entity : std::enable_shared_from_this<Entity> { int value = 42; VoidSlot get_print_slot() { // Here fastsignals::bind_weak() used instead of std::bind. return fastsignals::bind_weak(&Entity::print, weak_from_this()); } void print() { std::cout << "print called, num = " << value << std::endl; } }; int main() { VoidSignal event; auto entity = std::make_shared<Entity>(); event.connect(entity->get_print_slot()); // Here slot called - it prints `slot called, num = 42` event(); entity = nullptr; // Here nothing happens - no exception, no slot call. event(); }

两者行为一致:对象销毁后再次发射信号均“无任何事发生”。迁移时把std::bind(&Class::method, this)替换为fastsignals::bind_weak(&Class::method, weak_from_this())即可。

2.4 结果合并器(Result Combiners)的差异

原文档在此处仅给出章节标题。结合 combiners.h 可推断:FastSignals 默认且主要提供optional_last_value合并器——收集所有槽的返回值但只保留最后一个,并以std::optional包装(void 特化直接返回 void)。这与 Boost.Signals2 的optional_last_value语义一致;如果你的代码依赖其他 combiner(如first_value、last_value等),迁移时需要自行实现对应模板或调整逻辑。

Step 3:运行测试

完成代码修改后,运行你现有的全部自动化测试(单元、集成、系统、压力、基准、UI 测试)。按文档说法,大概率不会报错;若发现问题,请向 FastSignals 项目上报 issue。

七、测试与构建

FastSignals 自带完整的测试体系(见 tests 目录):

  • 单元测试libfastsignals_unit_tests:覆盖signal_tests.cpp、Function_tests.cpp、bind_weak_tests.cpp,使用的测试框架是仓库内置的 Catch2(catch.hpp);
  • 压力测试libfastsignals_stress_tests:signal_stress_tests.cpp用于验证并发场景下的线程安全行为,与文档“并发 connect/disconnect/emit 线程安全”的声明相互印证。

CMake 层面,FastSignals/CMakeLists.txt 定义库目标,cmake/functions.cmake 提供辅助构建函数。在 FreeCAD 中,它经由 src/3rdParty/CMakeLists.txt 被加入构建,业务代码只需#include <fastsignals/signal.h>即可使用(如 src/App/Application.h 所示)。

八、FreeCAD 中的实战形态

FreeCAD 在 App 核心层集中使用 FastSignals 实现文档与对象的事件总线。以 src/App/Application.h 为例:

fastsignals::signal<void (const Document&, bool)> signalNewDocument; fastsignals::signal<void (const Document&)> signalDeleteDocument; fastsignals::signal<void ()> signalDeletedDocument; fastsignals::signal<void (const Document&)> signalRelabelDocument; fastsignals::signal<void (const Document&)> signalRenameDocument; fastsignals::signal<void (const Document&)> signalActiveDocument; fastsignals::signal<void (const Document&)> signalSaveDocument; fastsignals::signal<void (const Document&, const std::string&)> signalStartSaveDocument; fastsignals::signal<void (const Document&)> signalUndoDocument; fastsignals::signal<void ()> signalUndo; fastsignals::signal<void (const Document&)> signalRedoDocument; fastsignals::signal<void ()> signalRedo; fastsignals::signal<void (const Document&, const App::Property&)> signalBeforeChangeDocument; fastsignals::signal<void (const Document&, const App::Property&)> signalChangedDocument; fastsignals::signal<void (const App::DocumentObject&)> signalNewObject; fastsignals::signal<void (const App::DocumentObject&)> signalDeletedObject; fastsignals::signal<void (const App::DocumentObject&, const App::Property&)> signalBeforeChangeObject; fastsignals::signal<void (const App::DocumentObject&, const App::Property&)> signalChangedObject;

这些信号覆盖了文档从创建、打开、保存、撤销/重做,到对象与属性变更的完整生命周期,供 Gui 层及其他模块订阅。此外,src/App/Application.cpp 中DocOpenGuard以fastsignals::signal<void ()>&引用信号,展示了在 guard 类中按引用持有信号并用于 RAII 通知的典型模式。

这种用法与本文第三节的基础示例完全同构:signal<void (Args...)>声明 →connect订阅 → 通过operator()发射。理解 FastSignals 的核心 API,就等于理解了 FreeCAD 核心层事件机制的钥匙。

九、小结

FastSignals 以纯 C++17、紧凑实现、Boost.Signals2 兼容 API 和 3~6 倍的发射性能提升(按原文档基准数据),成为 FreeCAD 事件系统的底层信号库。迁移路径清晰:创建别名头 → 重建修错(补 include、清理 void 槽 return、bind_weak替换track)→ 全量回归测试。若你的 C++17 项目正受 Boost.Signals2 性能与二进制体积困扰,直接在本仓库的 src/3rdParty/FastSignals 中阅读源码、测试与 docs 文档,即可快速落地。

【免费下载链接】FreeCADOfficial source code of FreeCAD, a free and opensource multiplatform 3D parametric modeler.项目地址: https://gitcode.com/GitHub_Trending/fr/FreeCAD

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询