Every non-trivial C++ application needs a way for components to communicate without tight coupling. The Observer pattern is the classical solution, but raw observer interfaces breed boilerplate: manual registration, deregistration, and lifetime management bugs. Signal/slot libraries solve this elegantly — letting objects emit signals that any connected slot can respond to, with automatic disconnection when either side is destroyed.

In this article, we compare four C++ signal/slot implementations: Boost.Signals2, sigslot, libsigc++, and nano-signal-slot. Each takes a different philosophical approach to the same problem, from Boost’s feature-complete heaviness to nano-signal-slot’s minimal C++17 elegance.

Why Signal/Slot Over Raw Callbacks?

Raw function pointer callbacks have been the default event mechanism in C for decades, but they break down in modern C++ applications:

  1. No lifetime tracking: If the callback target is destroyed, the signal holder has a dangling pointer — a guaranteed segfault at some unpredictable future time.
  2. No automatic disconnection: Manual unregistration is error-prone. Forget one unregister() call and you have a use-after-free.
  3. Member function binding: std::bind and lambdas work but obscure intent. Signal/slot libraries make member function connection idiomatic.
  4. Thread safety: Connecting slots from one thread while a signal fires from another requires synchronization that raw callbacks don’t provide.

Signal/slot libraries handle all four concerns transparently, turning what would be 50+ lines of manual tracking into a single .connect() call.

Library Comparison

FeatureBoost.Signals2sigslotlibsigc++nano-signal-slot
GitHub StarsPart of Boost902448440
LicenseBSL-1.0MITLGPL-3.0MIT
C++ StandardC++11C++14C++17C++17
Header-onlyNoYesMostlyYes
Thread SafetyBuilt-in mutex supportNo (single-threaded)OptionalNo
Slot Groups/OrderingYes (grouped slots)NoNoNo
Automatic Lifespan Trackingtrack() + shared_ptrVia sigslot::signalVia sigc::trackableNo (manual only)
Signal CombinersYesNoYes (accumulators)No
Return Value HandlingCombiner patternvoid-onlyReturn via accumulatorvoid-only
Connection BlockingYesYesYesNo

Boost.Signals2: The Feature-Complete Solution

Boost.Signals2 is the evolution of the original Boost.Signals library, adding thread safety and improved slot management. It’s the most feature-rich option — supporting slot groups, prioritized ordering, combiners that aggregate return values, and automatic disconnection via track().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <boost/signals2.hpp>
#include <boost/smart_ptr.hpp>
#include <iostream>

class DataProcessor {
public:
    // Signal with return value: each slot returns a bool
    using ValidateSignal = boost::signals2::signal<bool(const std::string&)>;

    void register_validator(const ValidateSignal::slot_type& slot) {
        m_validate_signal.connect(slot);
    }

    bool process(const std::string& data) {
        // All validators must pass (combiner returns false on first failure)
        auto result = m_validate_signal(data);
        return result.value_or(true);
    }

private:
    ValidateSignal m_validate_signal;
};

class InputValidator {
public:
    InputValidator(DataProcessor& proc) {
        m_connection = proc.m_validate_signal.connect(
            boost::bind(&InputValidator::validate, this, _1));
    }

    bool validate(const std::string& data) {
        return data.find("DROP TABLE") == std::string::npos;
    }

private:
    boost::signals2::connection m_connection;
};

Boost.Signals2’s track() mechanism is particularly valuable in complex applications: it accepts a shared_ptr and automatically disconnects the slot when the tracked object’s reference count drops to zero. No manual cleanup needed.

sigslot: Lightweight and Header-Only

sigslot by Pierre-Antoine Lacaze is a single-header C++14 implementation designed for minimal overhead. It generates no virtual calls, no heap allocations per connection, and is small enough (~800 lines) to read and understand in one sitting.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <sigslot/signal.hpp>
#include <iostream>

class Button {
public:
    sigslot::signal<> clicked;       // No arguments
    sigslot::signal<int, int> resized; // Two arguments
    sigslot::signal<const std::string&> text_changed;
};

class View {
public:
    View(Button& btn) {
        btn.clicked.connect(&View::on_click, this);
        btn.resized.connect(&View::on_resize, this);
    }

    void on_click() {
        std::cout << "Button clicked!" << std::endl;
    }

    void on_resize(int w, int h) {
        std::cout << "Resized to " << w << "x" << h << std::endl;
    }
};

// Usage
Button btn;
View view(btn);
btn.clicked();    // Prints: Button clicked!

sigslot’s simplicity is its strength. The library has zero external dependencies, compiles in under a second, and adds negligible binary size. For projects that want signal/slot semantics without Boost’s weight, sigslot is the clear winner.

libsigc++: The GTKmm Foundation

libsigc++ is the signal library that powers the entire GTKmm (C++ GTK) ecosystem. It’s battle-tested across thousands of Linux desktop applications and provides the most mature API of the four.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <sigc++/sigc++.h>
#include <iostream>

class TemperatureSensor : public sigc::trackable {
public:
    // Signal returning void with double parameter
    sigc::signal<void(double)> temperature_changed;

    void read() {
        double temp = /* hardware read */ 23.5;
        temperature_changed.emit(temp);
    }
};

class Display : public sigc::trackable {
public:
    Display(TemperatureSensor& sensor) {
        sensor.temperature_changed.connect(
            sigc::mem_fun(*this, &Display::on_temperature_changed));
    }

    void on_temperature_changed(double temp) {
        std::cout << "Current temperature: " << temp << "°C" << std::endl;
    }
};

// Accumulators for reducing return values
sigc::signal<int(int, int), sigc::nil, std::plus<int>> sum_signal;

libsigc++’s sigc::trackable base class provides automatic disconnection when the slot-holding object is destroyed — no shared_ptr needed. This is simpler than Boost’s track() but requires inheriting from sigc::trackable.

nano-signal-slot: Bare-Minimum C++17

nano-signal-slot takes minimalism to the extreme: it’s a single-header C++17 library under 400 lines. It provides signals, slots, and connections — nothing more. No thread safety, no combiners, no ordering.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <nano-signal-slot/nano_signal_slot.hpp>
#include <iostream>

Nano::Signal<void()> on_startup;
Nano::Signal<void(int, const std::string&)> on_error;

void log_error(int code, const std::string& msg) {
    std::cerr << "Error " << code << ": " << msg << std::endl;
}

int main() {
    auto conn = on_error.connect<&log_error>();
    on_error.fire(500, "Internal Server Error");

    // Manual disconnection
    conn.disconnect();
    on_error.fire(404, "Not Found");  // No output — disconnected
}

nano-signal-slot is ideal for embedded systems, game engines where you control the threading model, or any project where you want signal/slot semantics measured in tens of bytes, not kilobytes.

Decision Guide

RequirementBest Choice
Thread-safe signals in a multi-threaded serverBoost.Signals2
Header-only, no Boost dependency, C++14sigslot
GTK/Linux desktop applicationlibsigc++ (native GTKmm integration)
Minimal binary size, embedded, game enginenano-signal-slot
Complex signal return value aggregationBoost.Signals2 (combiners)

For most server-side C++ projects, sigslot hits the sweet spot: header-only, no dependencies, and complete enough for typical use cases. If you need thread safety, Boost.Signals2 is the only one that provides it out of the box.

For complementary reading on state management patterns, see our state machine libraries comparison. For messaging between distributed systems (not just in-process signals), our brokerless messaging guide covers ZeroMQ and nanomsg. If you’re building async event systems, our async I/O runtime comparison covers the I/O side of event-driven architectures.

Performance Characteristics and Overhead

Understanding the runtime cost of each signal/slot library helps make informed tradeoffs. Here’s what benchmarks reveal:

Boost.Signals2 adds approximately 150-200ns per signal emission on modern hardware (excluding slot execution time). The mutex overhead from thread safety is the largest contributor — if you compile with boost::signals2::signal<void(), boost::signals2::dummy_mutex>, that drops to ~80ns. Connection management (connect/disconnect) operations are O(log n) due to internal sorted slot groups.

sigslot clocks ~40-60ns per emission, making it the fastest of the four. Its zero-allocation design and lack of virtual dispatch explain the speed advantage. However, its std::function-based slot storage means connecting/disconnecting triggers heap allocations — fine for setup, but avoid hot-path connect/disconnect cycles.

libsigc++ emits at ~100-120ns and has the most mature accumulator infrastructure. Its slot invocation involves one virtual call (unavoidable with sigc::slot_base), adding ~15-20ns compared to sigslot’s template-only approach.

nano-signal-slot is nearly identical to sigslot in performance (~45-65ns) since it uses the same template-only, zero-allocation design. The main difference is nano-signal-slot’s simpler connection management, which is slightly faster but lacks connection blocking.

In practice, all four libraries are fast enough that signal dispatch is rarely the bottleneck. The cost of your actual slot handlers (database queries, file I/O, computation) will dominate by 100-1000x. Choose based on API ergonomics and safety features, not microbenchmarks.

Legacy Systems and Migration

If you’re migrating from an older signal library to a modern one, consider these compatibility paths:

From Boost.Signals (v1) to Boost.Signals2: Boost.Signals2 is a near drop-in replacement with the same API plus thread safety. Change your #include <boost/signals.hpp> to #include <boost/signals2.hpp> and add boost::signals2:: namespace qualifiers. Most code compiles unchanged.

From Qt Signals to sigslot: Qt signals require QObject inheritance and the MOC preprocessor. sigslot is pure C++ templates. Migration means replacing signals: and slots: declarations with sigslot::signal<> member variables and .connect() calls. This is a mechanical but manual process — there’s no automated conversion tool.

From homegrown callback systems: Replace your std::vector<std::function<...>> with a signal object. Add .connect() for registration, .emit() for firing, and remove manual unregistration code. The library handles lifetime management automatically.

FAQ

Why use signal/slot instead of std::function callbacks?

std::function manages a single callback. Signal/slot manages N-to-M connections: one signal can fire to many slots, and slots can connect to multiple signals. The library also handles automatic disconnection when objects are destroyed — a critical safety feature that std::function doesn’t provide.

Is Boost.Signals2 too heavy for embedded systems?

Yes. Boost.Signals2 pulls in significant parts of Boost (Bind, Function, MPL, TypeTraits, SmartPtr). For embedded or resource-constrained environments, nano-signal-slot (under 400 lines, zero dependencies) is the appropriate choice.

Can I mix sigslot with Qt signals?

Yes — they operate independently. Qt’s signals use the MOC (Meta-Object Compiler) preprocessor and QObject inheritance. sigslot is pure template-based C++ and doesn’t interfere with Qt’s signal system. Use Qt signals for UI updates, sigslot for non-Qt business logic.

How do signal/slot libraries handle threading?

Only Boost.Signals2 provides built-in thread safety via mutexes. sigslot, libsigc++, and nano-signal-slot assume single-threaded access. If you connect signals from multiple threads, either serialize access yourself or use Boost.Signals2 with its mutex template parameter.

Where can I get Docker Compose or installation files?

These are header-only or compile-from-source C++ libraries — not self-hosted services. Use your system package manager (e.g., apt install libboost-signals-dev libsigc++-3.0-dev) or integrate via CMake FetchContent. sigslot and nano-signal-slot are single headers you can drop directly into your project.


💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com