Тонкий механизм сигналов / событий C++ с семантикой перемещения для слотов
Я пытаюсь разработать систему сигналов и слотов в C++. Механизм несколько вдохновлен boost::signal, но должен быть проще. Я работаю с MSVC 2010, что означает, что некоторые функции C++11 доступны, но, к сожалению, нет вариативных шаблонов.
Во-первых, позвольте мне дать некоторую контекстную информацию. Я реализовал систему для обработки данных, которые генерируются различными аппаратными датчиками, подключенными к ПК. Каждый отдельный аппаратный датчик представлен классом, который наследуется от универсального класса Device. Каждый датчик запускается как отдельный поток, который получает данные и может пересылать их нескольким классам процессора (например, фильтрам, визуализаторам и т. Д.). Другими словами, Устройство - это сигнал, а Процессор - это слот или слушатель. Вся система сигнал / слот должна быть очень эффективной, так как датчики генерируют много данных.
Следующий код показывает мой первый подход для сигналов с одним аргументом. Можно добавить (скопировать) больше специализаций шаблона, чтобы включить поддержку большего количества аргументов. Пока что в коде ниже отсутствует безопасность потоков (для синхронизации доступа к slots_vec потребуется мьютекс).
Я хотел убедиться, что каждый экземпляр слота (то есть экземпляр процессора) не может использоваться другим потоком. Поэтому я решил использовать unique_ptr и std::move для реализации семантики перемещения для слотов. Это должно гарантировать, что если и только если слоты отключены или когда сигнал разрушен, слоты также разрушаются.
Мне интересно, если это "элегантный" подход. Любой класс, использующий класс Signal ниже, теперь может либо создать экземпляр Signal, либо наследовать от Signal для предоставления типичных методов (например, connect, emit и т. Д.).
#include <memory>
#include <utility>
#include <vector>
template<typename FunType>
struct FunParams;
template<typename R, typename A1>
struct FunParams<R(A1)>
{
typedef R Ret_type;
typedef A1 Arg1_type;
};
template<typename R, typename A1, typename A2>
struct FunParams<R(A1, A2)>
{
typedef R Ret_type;
typedef A1 Arg1_type;
typedef A2 Arg2_type;
};
/**
Signal class for 1 argument.
@tparam FunSig Signature of the Signal
*/
template<class FunSig>
class Signal
{
public:
// ignore return type -> return type of signal is void
//typedef typenamen FunParams<FunSig>::Ret_type Ret_type;
typedef typename FunParams<FunSig>::Arg1_type Arg1_type;
typedef typename Slot<FunSig> Slot_type;
public:
// virtual destructor to allow subclassing
virtual ~Signal()
{
disconnectAllSlots();
}
// move semantics for slots
bool moveAndConnectSlot(std::unique_ptr<Slot_type> >& ptrSlot)
{
slotsVec_.push_back(std::move(ptrSlot));
}
void disconnectAllSlots()
{
slotsVec_.clear();
}
// emit signal
void operator()(Arg1_type arg1)
{
std::vector<std::unique_ptr<Slot_type> >::iterator iter = slotsVec_.begin();
while (iter != slotsVec_.end())
{
(*iter)->operator()(arg1);
++iter;
}
}
private:
std::vector<std::unique_ptr<Slot_type> > slotsVec_;
};
template <class FunSig>
class Slot
{
public:
typedef typename FunParams<FunSig>::Ret_type Ret_type;
typedef typename FunParams<FunSig>::Arg1_type Arg1_type;
public:
// virtual destructor to allow subclassing
virtual ~Slot() {}
virtual Ret_type operator()(Arg1_type) = 0;
};
Дополнительные вопросы, касающиеся этого подхода:
1) Обычно сигнал и слоты будут использовать константные ссылки на сложные типы данных в качестве аргументов. С boost::signal необходимо использовать boost::cref для подачи ссылок. Я хотел бы избежать этого. Если я создам экземпляр Signal и Slot следующим образом, гарантируется ли передача аргументов в виде const refs?
class Sens1: public Signal<void(const float&)>
{
//...
};
class SpecSlot: public Slot<Sens1::Slot_type>
{
void operator()(const float& f){/* ... */}
};
Sens1 sens1;
sens1.moveAndConnectSlot(std::unique_ptr<SpecSlot>(new SpecSlot));
float i;
sens1(i);
2) boost::signal2 не требует типа слота (получателю не нужно наследовать от общего типа слота). На самом деле можно подключить любой функтор или указатель на функцию. Как это на самом деле работает? Это может быть полезно, если boost::function используется для подключения любого указателя функции или указателя метода к сигналу.
1 ответ
ПОМЕЩЕНИЕ:
Если вы планируете использовать это в большом проекте или в производственном проекте, мое первое предложение - не изобретать велосипед, а использовать Boost.Signals2 или альтернативные библиотеки. Эти библиотеки не так сложны, как вы думаете, и, вероятно, будут более эффективными, чем любое специальное решение, которое вы могли бы придумать.
Тем не менее, если ваша цель более дидактическая, и вы хотите немного поиграть с этими вещами, чтобы выяснить, как они реализованы, тогда я ценю ваш дух и постараюсь ответить на ваши вопросы, но не раньше, чем дать вам немного совет по улучшению.
КОНСУЛЬТАЦИИ:
Прежде всего, это предложение сбивает с толку:
"Методы подключения и отключения пока не являются поточно-ориентированными. Но я хотел убедиться, что каждый экземпляр слота (то есть экземпляр процессора) не может использоваться другим потоком. Поэтому я решил использовать unique_ptr
а такжеstd::move
реализовать семантику перемещения для слотов ".
На всякий случай, если вы думаете об этом ("но" в вашем предложении предполагает это), используяunique_ptr
на самом деле не спасает вас от необходимости защищать vector
слотов против данных гонок. Таким образом, вы все равно должны использовать мьютекс для синхронизации доступа к slots_vec
тем не мение.
Второй момент: с помощьюunique_ptr
Вы предоставляете исключительное право собственности на объекты слота для отдельного объекта сигнала. Если я правильно понимаю, вы утверждаете, что делаете это, чтобы не перепутать разные потоки с одним и тем же слотом (что заставит вас синхронизировать доступ к нему).
Я не уверен, что это разумный выбор с точки зрения дизайна. Прежде всего, это делает невозможным зарегистрировать один и тот же слот длянескольких сигналов(я слышал, вы возражаете, что вам это сейчас не нужно, но держитесь). Во-вторых, вы можете захотеть изменить состояние этих процессоров во время выполнения, чтобы адаптировать их реакцию к полученным сигналам. Но если у вас нет указателей на них, как бы вы это сделали?
Лично я бы по крайней мере пойти наshared_ptr
, что позволит автоматически управлять временем жизни ваших слотов; и если вы не хотите, чтобы несколько потоков связывались с этими объектами, просто не предоставляйте им доступ к ним. Просто избегайте передачи общего указателя на эти потоки.
Но я бы пошел еще на один шаг дальше: если ваши слоты являются вызываемыми объектами, как это кажется, то я бы отбросил shared_ptr
на всех и скорее пользуйся std::function<>
заключить их в капсулу Signal
учебный класс. То есть я бы просто держал vector
из std::function<>
объекты, которые будут вызываться каждый раз, когда испускается сигнал. Таким образом, у вас будет больше возможностей, чем просто наследование отSlot
для того, чтобы настроить обратный вызов: вы можете зарегистрировать простой указатель функции или результатstd::bind
или просто любой функтор, который вы можете придумать (даже лямбда).
Теперь вы, вероятно, видите, что это очень похоже на дизайн Boost.Signals2. Пожалуйста, не думайте, что я не игнорирую тот факт, что вашей первоначальной целью было создать что-то более тонкое, чем это; Я просто пытаюсь показать вам, почему современная библиотека спроектирована таким образом и почему в конце концов имеет смысл прибегнуть к ней.
Конечно, регистрация std::function
объекты, а не умные указатели в вашемSignal
класс заставит вас позаботиться о времени жизни тех функторов, которые вы размещаете в куче; однако это не обязательно должно быть обязанностью Signal
учебный класс. Для этой цели вы можете создать класс-обертку, который мог бы хранить общие указатели на функторы, которые вы создаете в куче (например, экземпляры классов, производных от Slot
) и зарегистрируйте их наSignal
объект. С некоторой адаптацией это также позволит вам регистрировать и отключать слотыиндивидуально, а не "все или ничего".
ОТВЕТЫ:
Но давайте теперь предположим, что ваши требования есть ивсегда будут(последняя часть действительно трудно предвидеть) действительно такими, что:
- Вам не нужно регистрировать один и тот же слот для нескольких сигналов;
- Вам не нужно менять состояние слота во время выполнения;
- Вам не нужно регистрировать различные типы обратных вызовов (лямбда-выражения, указатели функций, функторы, ...);
- Вам не нужно выборочно отключать отдельные слоты.
Тогда вот ответы на ваши вопросы:
Q1:"[...] Если я создам экземпляр Signal и экземпляр Slot следующим образом, гарантируется ли передача аргументов как const refs?"
A1: Да, они будут передаваться как постоянные ссылки, потому что все на вашем пути пересылки является постоянной ссылкой.
Q2:"[В Boost.Signals2] можно фактически подключить любой функтор или указатель на функцию. Как это на самом деле работает? Это может быть полезно, если boost::function используется для подключения любого указателя функции или указателя метода к сигналу"
A2: он основан на boost::function<>
шаблон класса (который позже сталstd::function
и должен поддерживаться как таковой в VS2010, если я правильно помню), который использует методы стирания типов, чтобы обернуть вызываемые объекты различных типов, но идентичные подписи. Если вам интересно узнать подробности реализации, см.boost::function<>
или взглянуть на реализацию MS std::function<>
(должно быть очень похоже).
Надеюсь, это вам немного помогло. Если нет, не стесняйтесь задавать дополнительные вопросы в комментариях.
Вот мой подход:
Он намного легче, чем boost, но не обрабатывает агрегированные ответы.
Я думаю, что это элегантно в использовании shared_ptr для владельца обратного вызова и weak_ptr для повышения сигнала, что гарантирует, что обратный вызов все еще существует.
Мне также нравится, как он самоочищает мертвые обратные вызовы weak_ptr.
template <typename... FuncArgs>
class Signal
{
using fp = std::function<void(FuncArgs...)>;
std::forward_list<std::weak_ptr<fp> > registeredListeners;
public:
using Listener = std::shared_ptr<fp>;
Listener add(const std::function<void(FuncArgs...)> &cb) {
// passing by address, until copy is made in the Listener as owner.
Listener result(std::make_shared<fp>(cb));
registeredListeners.push_front(result);
return result;
}
void raise(FuncArgs... args) {
registeredListeners.remove_if([&args...](std::weak_ptr<fp> e) -> bool {
if (auto f = e.lock()) {
(*f)(args...);
return false;
}
return true;
});
}
};
Применение:
Signal<int> bloopChanged;
// ...
Signal<int>::Listener bloopResponse = bloopChanged.add([](int i) { ... });