MiddleКодИногдаЕщё не отвечали
Реализуйте минимальный механизм signal/slot с использованием лямбд и std::function
Реализуйте минимальный механизм signal/slot. Signal<Args...> позволяет вызывающим подключать колбэки через connect (включая захватывающие лямбды), вызывать все через emit и отключать конкретный через disconnect.
Ограничения:
- Слоты должны принимать захватывающие лямбды, а не только указатели на функции.
connectвозвращает token;disconnect(token)удаляет ровно этот слот.emit(args...)вызывает каждый подключённый слот по порядку.
#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;
// ваш код здесь
};
Допишите реализацию.
Храните колбэки в std::vector<std::function<void()>> с token-id для disconnect, и пробегайте на emit(). Лямбды с захватами подходят — std::function стирает тип замыкания.
- ✗Захват локальных переменных по ссылке, когда лямбда живёт дольше области видимости
- ✗Копирование
std::functionс большими захватами вместо предпочтительногоstd::move - ✗Игнорирование того, что
std::functionобычно даёт около 10-20% накладных расходов относительно прямого вызова из-за выделения памяти и стирания типа
- →Как сделать подключение слотов потокобезопасным?
- →Что такое
std::move_only_functionи когда его стоит предпочестьstd::function?
Оглавление
Эталонная реализация
#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;
};
Пример использования
Signal<int> on_value_changed;
auto t1 = on_value_changed.connect([](int v) {
std::cout << "слушатель A: " << v << '\n';
});
on_value_changed.connect([](int v) {
std::cout << "слушатель B: " << v << '\n';
});
on_value_changed.emit(42); // печатает A и B
on_value_changed.disconnect(t1);
on_value_changed.emit(99); // печатает только B
Компромиссы
| Вариант | Плюс | Минус |
|---|---|---|
std::function | Стирание типа, высокая гибкость | Выделение памяти в куче, ~10-20 нс накладных расходов на вызов |
Шаблонный параметр F | Нулевые накладные расходы | Для одного экземпляра сигнала доступен только один тип callable |
std::move_only_function (C++23) | Только перемещение, немного быстрее | Пока поддерживается не везде |
Потокобезопасность
Если обернуть slots_ в std::shared_mutex, несколько потоков смогут одновременно вызывать emit, а добавлять и удалять слоты будет только один поток за раз. Для lock-free варианта потребуется std::atomic<std::shared_ptr<std::vector<Entry>>> и copy-on-write.
Оглавление