MiddleCodeOccasionalNot answered yet
Implement a minimal signal/slot mechanism using lambdas and std::function
Implement a minimal signal/slot mechanism. A Signal<Args...> lets callers connect callbacks (including capturing lambdas), emit to invoke all of them, and disconnect a specific one.
Constraints:
- Slots must accept capturing lambdas, not just plain function pointers.
connectreturns a token;disconnect(token)removes exactly that slot.emit(args...)calls every connected slot in order.
#include <functional>
#include <vector>
#include <cstdint>
template<typename... Args>
class Signal {
public:
using Slot = std::function<void(Args...)>;
using Token = std::uint64_t;
Token connect(Slot slot);
void disconnect(Token token);
void emit(Args... args) const;
// your code here
};
Write the implementation.
Store callbacks as std::vector<std::function<void()>> keyed by token, and iterate on emit(). Lambdas with captures fit naturally — std::function type-erases the closure.
- ✗Capturing local variables by reference when the lambda outlives the scope
- ✗Copying
std::functionobjects that wrap large captures — preferstd::move - ✗Forgetting that
std::functionhas ~10-20% overhead vs a direct call due to heap allocation and type erasure
- →How would you make the slot connection thread-safe?
- →What is
std::move_only_functionand when would you choose it overstd::function?
Contents
Reference implementation
#include <functional>
#include <vector>
#include <cstdint>
template<typename... Args>
class Signal {
public:
using Slot = std::function<void(Args...)>;
using Token = std::uint64_t;
Token connect(Slot slot) {
Token token = next_token_++;
slots_.push_back({ token, std::move(slot) });
return token;
}
void disconnect(Token token) {
slots_.erase(
std::remove_if(slots_.begin(), slots_.end(),
[token](const Entry& e) { return e.token == token; }),
slots_.end());
}
void emit(Args... args) const {
for (const auto& entry : slots_) {
entry.slot(args...);
}
}
private:
struct Entry { Token token; Slot slot; };
std::vector<Entry> slots_;
Token next_token_ = 1;
};
Usage example
Signal<int> on_value_changed;
auto t1 = on_value_changed.connect([](int v) {
std::cout << "listener A: " << v << '\n';
});
on_value_changed.connect([](int v) {
std::cout << "listener B: " << v << '\n';
});
on_value_changed.emit(42); // prints A and B
on_value_changed.disconnect(t1);
on_value_changed.emit(99); // prints only B
Trade-offs
| Design | Pro | Con |
|---|---|---|
std::function | Type-erased, flexible | Heap alloc, ~10-20 ns overhead per call |
Template param F | Zero overhead | Only one callable type per signal instance |
std::move_only_function (C++23) | Move-only, slightly faster | Not yet widely supported |
Thread safety
Wrapping slots_ in a std::shared_mutex lets multiple threads emit concurrently but only one thread add/remove slots at a time. For lock-free use, use std::atomic<std::shared_ptr<std::vector<Entry>>> plus copy-on-write.
Contents