Как стать автором
Обновить

Комментарии 35

В Qt есть The Event System и Signals & Slots. Почему для сравнения берутся сигналы из Qt, а не сигналы из Boost?

В Qt есть The Event System и Signals & Slots.

Данная реализация как раз представляет собой нечто близкое к 'Signals & Slots', а не к 'The Event System'; последнее ближе к очереди (циклу) событий, там объекты взаимодействуют через цикл событий, а не напрямую. На мой взгляд, это всё же другой подход.
Почему для сравнения берутся сигналы из Qt, а не сигналы из Boost?

Вообще не было цели сравнивать хоть с чем-то. Это, скорее, просто бонус, вызванный тем, что с сигналами/слотами Qt мне приходилось работать (а с сигналами из boost нет).
В контексте GUI ожидается граф сцены, в ивенте содержатся координаты мыши, а ивент проходится по графу сцены.

Тогда очевидный вопрос зачем? Когда есть множество реализаций тех же сингалов?
В контексте GUI ожидается граф сцены, в ивенте содержатся координаты мыши, а ивент проходится по графу сцены.

Как я уже сказал, я не считаю этот подход равнозначным. Как Вы верно заметили, даже в Qt реализованы они оба (и The Event System, и Signals & Slots). Кроме того, use case'ов много (не только лишь GUI), не везде удобно одно и то же.
Тогда очевидный вопрос зачем? Когда есть множество реализаций тех же сингалов?

Согласен. Как минимум, когда не хочется/можется подключать библиотеку с очередной реализаций сигналов. Возможно, кому-то будет удобно и полезно. Плюс, небольшая демонстрация разных возможностей С++ (но это тоже скорее доп).
Дочитав до потокобезопасности возник вопрос. Зачем?
Так, вроде бы, там написано, зачем. Вы с чем-то из этого не согласны?
невозможность нахождения декларации класса в .cpp-файле;
Это неправда: у меня декларация находится в cpp-файле — moc генерируется! Всё работает! Можете убрать из недостатков ;)
А у Вас класс, описанный в .cpp, имеет директиву Q_OBJECT (и соответственно, её применение в виде, например, сигналов)?
Конечно имеет Q_OBJECT.
А какая у Вас версия Qt? Перепроверил — всё подтвердилось. Я сейчас использую Qt5.6.1-1 (надо бы, конечно, обновиться...). Компилятор — VS++14 (соответственно, и MSBuild в качестве сборщика).
Ошибка на этапе сборке.
При описанном в cpp-файле классе с директивой Q_OBJECT выдаётся предупреждение
Warning MSB8017 A circular dependency has been detected while executing custom build commands for item "GeneratedFiles\Debug\filename.moc". This may cause incremental build to work incorrectly.
и все методы, которые должны быть сгенерированы moc'ом (metaObject, qt_metacast и т.д.), становятся unresolved, вызывая соответствующие ошибки. Кроме того в Generated Files, где под каждую конфигурацию должен присутствовать сгенерированный файлик moc_filename.cpp, находится только несуществующий filename.moc.
Возможно, дело именно в MSBuild'е или его взаимодействии с Qt. Если это и вправду так, нужно будет действительно убрать этот пункт из недостатков.
Обычно решение — это включить filename.moc в конце файла filename.cpp, файл должен генерироваться при сборке.
Это действительно решило проблему сборку (хоть предупреждение сборщика и осталось).
Спасибо; недостаток убираю.
Qt 5.11.1, GCC 8.2.1, Linux
C++11 это вы преуменьшаете :) std::shared_mutex ----> C++17.

Пара поверхностных замечаний (толком код не смотрел):
— Не собралось под GCC из-за некоторого несоответствия стандарту (например в шаблонном производном классе нельзя использовать unqualified type name из шаблонного базового класса — name lookup не обязан туда заглядывать). См пулл-реквест;
— У меня впечатление, что shared_ptr для Holder не обязателен и я бы попытался заменить его на unique_ptr в списке обработчиков + сырой readonly указатель во всех остальных местах.
— shared_ptr(new XXX) -----> лучше использовать std::make_shared(XXX) (классика)
— Некоторые внутренние типы лучше спрятать из публичного интерфейса (eg. IsMethodParamsCompatible)
C++11 это вы преуменьшаете :) std::shared_mutex ----> C++17.
Действительно, размахнулся что-то.
Не собралось под GCC из-за некоторого несоответствия стандарту
Это всё моя привычка разработки под VC. Надо будет поисправлять.
У меня впечатление, что shared_ptr для Holder не обязателен и я бы попытался заменить его на unique_ptr в списке обработчиков + сырой readonly указатель во всех остальных местах.
Немного не понял. В списке обработчиков как раз не Holder, а EventHandler, в котором уже Holder. Вы всё-таки при EventHandler?
лучше использовать std::make_shared
там, где применяется не он, используется private конструктор (в Holder'ах, например, это сделано для правильной инициализации поля m_me).
Некоторые внутренние типы лучше спрятать из публичного интерфейса
так, вроде убраны же в анонимные namespace'ы.
А вообще, за замечания спасибо)
Вы всё-таки при EventHandler?

Упс, да, EventHandler конечно.
так, вроде убраны же в анонимные namespace'ы.

Но они экспортируются в публичный заголовочный файл eventhandling.hpp. Такие вещи лучше прятать в cpp файлы.
Такие вещи лучше прятать в cpp файлы.
А как это сделать в случае с шаблонами? Вы можете привести небольшой пример?

С шаблонными классами это невозможно (виноват, проглядел <> в IsMethodParamsCompatible), кроме ест-но переноса IsMethodParamsCompatible в cpp файл с некрасивым захардкоживанием специализаций в *.cpp файле:


// methodeventhandler.hpp
template<class TMethodHolder, class ...TParams>
class MethodEventHandler : public AbstractEventHandler<TParams...>
{
        //...
        virtual void call( TParams... params ) override; // прячем определение в cpp файл
       // ...
};

// methodeventhandler.cpp
template<class TMethodHolder, class ... TParams>
struct IsMethodParamsCompatible {
// определение класса...
};

template<class TMethodHolder, class ...TParams>
void MethodEventHandler<TMethodHolder, TParams...>::call(TParams...params)
{
// тело ф-и, скопированное из hpp
    static_assert( IsMethodParamsCompatible<TMethodHolder, TParams...>::value, "Event and method arguments are not compatible" );

    ( m_methodHolder->m_object.*m_methodHolder->m_method )( params... );
}

// ХАРДКОД (принудительное инстанцирование заранее известного типа).
// Без кода ниже будет ошибка линковки при сборке test.cpp -
// unresolved symbol MethodEventHandler<MethodHolder<ClassHandler, int, unsigned int>, unsigned int>::call(unsigned int)
class ClassHandler;

MethodEventHandler<events::handlers::MethodHolder<ClassHandler, int, unsigned int>, unsigned int> test(nullptr);
Спасибо за поддержку совместимости с GCC!
Возможно я не внимательно читал, но зачем это всё написано? В Forms .Net C# это есть, в MFC/ATL тоже проблем с обработчиками событий нет. Или это всё ради кроссплатформенности и Qt?
Не очень понятно, при чём здесь C# (которым я, кстати, во многом вдохновлялся), если в нём, в отличие от C++, данный механизм встроен в язык. Насчёт «ради Qt» тоже не очень понятно, потому что данная реализация позволяет как раз не использовать сторонние библиотеки и фреймворки (по крайней мере, только ради сигналов).
Мы в своём проекте завезли свои сигналы без зависимостей, дело то не хитрое.
Блин да очевидно зачем, полноценный легковесный механизм эвентов позволяющий реализовывать высокоуровневую бизнес логику без привязки к какому-то стороннему фреймворку же.
За показ хода мысли отдельное спасибо автору.
Мне, в свое время, понравилась статья Потокобезопасные сигналы, которыми действительно удобно пользоваться

Потенциально, вызовы трёх возможных функций — добавления, удаления и перебора (при срабатывании события) обработчиков — возможны из разных потоков в случайные моменты времени. Это создаёт целое поле возможностей по их «пересечению» во времени, «накладыванию» их исполнения друг на друга и падению программы в итоге. Попробуем избежать этого; мьютексы — наше всё.

Честно говоря не вижу как решена проблема с параллельным выполнением «перебора». Или вызовом «перебора» из какого-то обработчика. Похоже что такие ситуации никак не накрыты.

Иными словами оператор () не потокобезопасный и неентерабельный. Ну и нет нет никаких гарантий с точки зрения исключений в обработчиках.

       void operator()( TParams... params )
        {
            m_handlerListMutex.lock_shared();
            
            m_isCurrentItRemoved = false;
            m_currentIt = m_handlers.begin();
            while( m_currentIt != m_handlers.end() )
            {
                m_handlerListMutex.unlock_shared();
                // !!!! в следующей строке может случиться все что угодно !!!!
                ( *m_currentIt )->call( params... );
                m_handlerListMutex.lock_shared();

                if( m_isCurrentItRemoved )
                {
                    m_isCurrentItRemoved = false;

                    TEventHandlerIt removedIt = m_currentIt;
                    ++m_currentIt;

                    deleteHandler( removedIt );
                }
                else
                {
                    ++m_currentIt;
                }
            }

            m_handlerListMutex.unlock_shared();
        }
Спасибо за важное замечание, кое-что подправил (в частности, параллельный вызов события).
Ну и нет нет никаких гарантий с точки зрения исключений в обработчиках
А вот тут не совсем понятно, почему событие должно обрабатывать исключения в клиентском коде (если можно так назвать обработчики).
А вот тут не совсем понятно, почему событие должно обрабатывать исключения в клиентском коде (если можно так назвать обработчики).


Обрабатывать чужие исключения конечно не нужно. Вопрос, в каком состоянии будет евент, если произойдет исключение в обработчике. Про гаратнии можно почитать здесь Exception_safety. Стандартные контейнеры вроде в большинстве случаев имеют Strong exception guarantee. Для класса общего пользования иметь Basic exception guarantee вполне неплохо.

        void operator()( TParams... params )
        {
            TMyHandlerRunner newHandlerRunner( m_core );

            m_core.coreMutex.lock_shared();
            auto it = m_handlerRunners.insert( m_handlerRunners.end(), &newHandlerRunner );
            m_core.coreMutex.unlock_shared();
             //
            // если исключение бросится здесь
            //
            newHandlerRunner.run( params... );
            //
            // то вот это все не отработает и в m_handlerRunners застрянет указатель на локальный newHandlerRunner 
            //
            m_core.coreMutex.lock_shared();
            m_handlerRunners.erase( it );
            m_core.coreMutex.unlock_shared();
        }

Ну и удалять обработчик, если он сейчас работает нельзя. Если это функтор и в середине его работы происходит отписка и он дальше выполняет какой-то код, то мы в момент отписки удалим функтор и весь остальной код фунтора будет работать на удаленном объекте. Здесь надо помечать обрабртчик на удаление, а удалять в функции рассылки (при условии что он не рассылается в паралельном цикле рассылки)

Вообще в реализациях евентов есть подводные камни, которые в стандартных реализациях решены или документированы (в большинстве своем).

Хотя мы используем самописный евент, который за годы эксплуатации не раз подправляли.
Большое спасибо за разъяснение.
Явно есть, куда двигаться дальше.
А как Вы сравниваете слоты? Можете механизм сравнения описать словами?
Этому был посвящён целый пункт.
Но если вкратце: функторы сравниваются по адресу объекта, методы (функции-члены) — по адресу экземпляра класса (которому они принадлежат) и своему адресу.
Естественно, сравниваются только однотипные объекты: как функторы, так и методы классов. Разнотипные обработчики считаются неравными.
Т.е., я не смогу использовать ф-ию или лямбду в качестве слота?
Наоборот, сможете. И объект-функтор, и лямбда-выражение, и экземпляр std::function, и отдельную функцию (ну, и метод класса/структуры, конечно).
Звучит неплохо :) Еще один вопрос, извините, что надоедаю. Как Вы сравниваете два слота, созданных на основе лямбда-функций?
Все объекты сравниваются по значению, если это возможно; в противном случае происходит сравнение по адресу. Конкретно лямбды сравниваются по адресу.
Признайтесь, вдохновение в дотнетовских делегатах не черпали?
Признаю)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации