Тонкий механизм сигналов / событий c ++ с семантикой перемещения для слотов

Я пытаюсь разработать систему сигналов и слотов на С ++. Этот механизм в некоторой степени вдохновлен boost :: signal, но должен быть проще. Я работаю с MSVC 2010, что означает, что некоторые функции C ++ 11 доступны, но, к сожалению, вариативные шаблоны - нет.

Во-первых, позвольте мне дать некоторую контекстную информацию. Я реализовал систему для обработки данных, которые генерируются различными аппаратными датчиками, подключенными к компьютеру. Каждый аппаратный датчик представлен классом, который наследуется от универсального класса Device. Каждый датчик запускается как отдельный поток, который получает данные и может пересылать их нескольким классам процессора (например, фильтрам, визуализаторам и т. Д.). Другими словами, Устройство - это сигнал, а Процессор - это слот или слушатель. Вся система сигнал / слот должна быть очень эффективной, так как датчики генерируют много данных.

В следующем коде показан мой первый подход к сигналам с одним аргументом. Можно добавить (скопировать) дополнительные специализации шаблонов, чтобы включить поддержку большего количества аргументов. Безопасность потоков пока отсутствует в приведенном ниже коде (для синхронизации доступа к slots_vec потребуется мьютекс).

Я хотел убедиться, что каждый экземпляр слота (т.е. экземпляр процессора) не может использоваться другим потоком. Поэтому я решил использовать unique_ptr и std :: move для реализации семантики перемещения для слотов. Это должно гарантировать, что если и только если слоты отключены или когда сигнал разрушен, слоты также будут разрушены.

Мне интересно, является ли это «элегантным» подходом. Любой класс, использующий приведенный ниже класс Signal, теперь может либо создать экземпляр Signal, либо наследовать от Signal для предоставления типичных методов (например, подключения, излучения и т. Д.).

#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 используется для подключения любого указателя функции или указателя метода к сигналу.


person spinxz    schedule 22.01.2013    source источник
comment
Вы смотрели sigslot?   -  person paddy    schedule 22.01.2013
comment
Для управления временем жизни слотов boost :: signal2 предлагает метод slot :: track. См. здесь.   -  person spinxz    schedule 10.04.2013


Ответы (2)


ПОМЕЩЕНИЕ:

Если вы планируете использовать это в большом проекте или в производственном проекте, мое первое предложение - не изобретать велосипед и лучше использовать 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. С некоторой адаптацией это также позволит вам регистрировать и отключать слоты индивидуально, а не "все или ничего".

ОТВЕТЫ: ​​

Но давайте теперь предположим, что ваши требования есть и всегда будут (последнее действительно трудно предвидеть) действительно такими, что:

  1. Вам не нужно регистрировать один и тот же слот для нескольких сигналов;
  2. Вам не нужно изменять состояние слота во время выполнения;
  3. Вам не нужно регистрировать разные типы обратных вызовов (лямбды, указатели на функции, функторы, ...);
  4. Вам не нужно выборочно отключать отдельные слоты.

Тогда вот ответы на ваши вопросы:

Q1: "[...] Если я создам экземпляр Signal и экземпляр Slot следующим образом, гарантировано ли, что аргументы будут переданы как const refs?"

A1: Да, они будут передаваться как постоянные ссылки, потому что все на вашем пути пересылки является постоянной ссылкой.

Q2: "[В Boost.Signals2] фактически можно подключить любой функтор или указатель на функцию. Как это на самом деле работает? Это может быть полезно, если boost :: function используется для подключения любого указателя функции или указателя метода к сигналу "

A2: он основан на шаблоне класса boost::function<> (который позже стал std::function и должен поддерживаться как таковой в VS2010, если я правильно помню), который использует методы стирания типа, чтобы обернуть вызываемые объекты разных типов, но с идентичными сигнатурами. Если вам интересны подробности реализации, см. реализацию boost::function<> или взгляните на реализацию std::function<> в MS (она должна быть очень похожей).

Надеюсь, это вам немного помогло. Если нет, не стесняйтесь задавать дополнительные вопросы в комментариях.

person Andy Prowl    schedule 22.01.2013
comment
Большое спасибо за подробный и подробный ответ. Вы совершенно правы, что для синхронизации доступа к slots_vec следует использовать мьютекс. Я отредактировал свой вопрос для уточнения. - person spinxz; 22.01.2013
comment
Вы также правы в том, что обычно следует использовать существующую реализацию. Обсуждение того, какую библиотеку сигналов / слотов использовать, можно найти здесь и здесь. - person spinxz; 22.01.2013
comment
До сих пор я использовал библиотеку boost signal2, но ее критиковали за низкую производительность. Основная причина моей собственной реализации заключается в том, что я использую интеллектуальные указатели для управления сигналами и слотами. Но функция connect () boost :: signal не работает с (умными) указателями. Следовательно, мне пришлось разыменовать указатель на слот, чтобы подключить его: mysig.connect(*sharedPtr_to_my_slot). Это несколько уродливо, и если счетчик ссылок равен 0, а сигнал все еще может использовать слот ... - person spinxz; 22.01.2013

Вот мой подход:

Это намного легче, чем ускорение, но не обрабатывает агрегированные ответы.

Я думаю, что это элегантно в использовании 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) { ... });
person johnb003    schedule 10.10.2019