Костылик для сигнал-слот системы в Qt

a35754d5cac944259cacbc0b050eedd2.png

Привет всем. Я хочу рассказать вот о чём… Недели две назад впервые понадобилось работать с GUI под С++ и, погуглив малость, я решил использовать Qt. Его все жутко хвалили, да и вообще на первый взгляд выглядел он весьма достойно.

На второй взгляд Qt оказался тоже годной штукой, но из-за некоторых ограничений реализации его мето-объектного компилятора пришлось-таки конструировать небольшие костылики. В данном очерке (думаю, слово «очерк» подойдёт лучше, ибо по объёму это на статью не тянет) хочу рассказать о том, как я решал возникающие проблемы.

Как всё начиналось

Началось всё с того, что мне было необходимо сделать шаблонный класс-контроллер в рамках реализации MVC-архитектуры в моей библиотечке. Контроллер должен был взаимодействовать с разнотипным целочисленными данными, связывая с ними QSpinBox в качестве GUI. Если отбросить всякую шелуху, вышло что-то вот такое:

Шаблонный контроллер
template< typename T_IntegralType >
class IntegralController {
private:
        T_IntegralType *_modelField;
        QSpinBox *_view;
        ...

public:
        QSpinBox *getView() {
                if (!_view) {
                        _view = new QSpinBox();
                        /* Подписаться на событие от вьюшечки*/
                }
                return _view;
        }
        . . .

private:
        // Метод, который должен срабатывать по событию от вьюшечки
        void setValue(T_IntegralType inValue) { * modelField = inValue; }
};


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

Примечание про model-view подход
Есть ещё такая вещь, как система делегатов в рамках model-view подхода в Qt, которая позволяет обрабатывать пересылку данных между view и model через реализацию интерфейсов, без системы сигналов-слотов. По определённым причинам, мне не удалось нормально использовать model-view Qt у себя в библиотеке.

Для того, чтобы какой-либо класс мог предоставлять слоты для сигнал-слот системы, необходимо было чтобы этот класс наследовался от класса QObject и включал в себя макрос Q_OBJECT. Зачем? Тогда я не парился с тем, чтобы разобраться. Нужно — значит нужно. Не мудрствуя лукаво, добавил требуемые вещички в свой шаблонный класс и описал реализацию обработки событий GUI:

Шаблонный контроллер с обработкой событий
template< typename T_IntegralType >
class IntegralController : public QObject {
        Q_OBJECT
private:
        T_IntegralType *_modelField;
        QSpinBox *_view;
        ...

public:
        QSpinBox *getView() {
                if (!_view) {
                        _view = new QSpinBox();
                        QObject::connect(_view, SIGNAL(valueChanged(int)),
                                        this, SLOT(valueChanged(int));
                }
        }
        . . .

private slots:
        void valueChanged(int inValue) { *_modelField = inValue; }
};


Всё собралось. Отлично, подумал я, какой же я молодец! — и продолжил пилить библиотеку, на время позабыв об этом шаблонном контроллере. Конечно, зря поторопился и вообще рано радовался, ибо был нефига не молодцом. Едва я попробовал собрать код, использующий специализацию шаблонного контроллера, как посыпались сообщения линковщика об ошибках, вот в таком духе:

Ошибки линковки
unresolved external symbol "public: virtual struct QMetaObject const * __cdecl TClass < int >::metaObject(void)const " (?metaObject@?$TClass@H@@UEBAPEBUQMetaObject@@XZ)

unresolved external symbol "public: virtual void * __cdecl TClass < int >::qt_metacast(char const *)" (?qt_metacast@?$TClass@H@@UEAAPEAXPEBD@Z)

unresolved external symbol "public: virtual int __cdecl TClass < int >::qt_metacall(enum QMetaObject::Call,int,void * *)" (?qt_metacall@?$TClass@H@@UEAAHW4Call@QMetaObject@@HPEAPEAX@Z)

Было очевидно, что я делаю что-то не так с мета-объектной системой. Пришлось перечитывать про MOC. Дело оказалось в том, что MOC, когда проходится по исходникам, генерирует дополнительные cpp-файлы, в которых создаётся реализация нужных для работы мета-объектой системы методов. С шаблонами эта система умеет работать очень криво, плохо генерируя этот самый метаобъектный код — MOC либо просто игнорирует шаблонные классы при генерации кода, либо воспринимает их как обычные классы, отбрасывая шаблонные аргументы, что по понятным причинам вызывало указанные выше проблемы.

Ещё про шаблоны и Qt
По поводу того, почему не стоит пользоваться шаблонами вместе с сигнал-слот системой есть даже отдельная статья в доке. К сожалению, в моём случае речь шла не об оптимальности, а о приличном сокращении объёма кода и избегании большого объёма копи-пасты — так что для меня пункты из этой статьи не подходили.

При дальнейшем ознакомлении с MOC, обнаружилось существование ещё нескольких ограничений на использование подконтрольных MOC классов. Самое неприятной из них — невозможность описания полноценно управляемых MOC наследников QObject во вложенных классах. А я люблю делать вложенные классы, разбивая области ответственности большого класса между живущими внутри него классиками поменьше. Да, знаю, тру-программисты используют неймспейсы, но при этом, на мой взгляд, загромождается контекст программы, да и семантика вложенных классов отличается от семантики неймспейсов (в первом случае мы строем иерархию отношений между классами, во втором — просто их группируем по каком-то признаку).

В общем, с учётом того, что от MOC-системы мне была нужна лишь возможность подписываться на события через статический метод QObject: connect (…), я решил написать небольшой костыль… Совсем небольшой, маленький костылик.

Про костылик

Идея была проста — сделать мини-класс, который наследовал бы QObject и полностью подходил бы для MOC в смысле регистрации класса для корректной работы в рамках сигнал-слот системы. Данный мини-класс предоставлял бы также метод, позволяющий связывать Qt-независимый коллбек с вызовом слота в этом вспомогательном классе.

Звучит сложно, пример будет яснее, я надеюсь. В коде для обработки событий от QSpinBox это выглядело вот так:

Костылик для событий от QSpinBox
class ValueChangedWorkaround : public QObject {
        Q_OBJECT
public:
        // Тип-функтор, реализующий коллбек. В моём случае это
        // был FastDelegate (ссылка на либу в этом спойлере, ниже)
        typedef fastdelegate::FastDelegate1 < int > Callback;

private:
        Callback _callback;

public:
        ValueChangedWorkaround() : _callback() { }

        void bind(QSpinBox *inSpinBox, const Callback &inCallback) {
                _callback = inCallback;
                QObject::connect(inSpinBox, SIGNAL(valueChanged(int)),
                                this, SLOT(valueChanged(int));
        }

private slots:
        void valueChanged(int inValue) { _callback(inValue); }
};

Про FastDelegate вообще
FastDelegate на GitHub

Я использовал этот код в контроллере — и всё заработало:

Контроллер с использованием костылика
template< typename T_IntegralType >
class IntegralController {
private:
        typedef IntegralController< IntegralType > OwnType;

        T_IntegralType *_modelField;
        QSpinBox *_view;
        ValueChangedWorkaround _valueChangedWorkaround;
        ...

public:
        QSpinBox *getView() {
                if (!_view) {
                        _view = new QSpinBox();
                        _valueChangedWorkaround.bind(_view,
                                ValueChangedWorkaround::Callback(
                                        &OwnType::valueChanged));
                }
        }
        . . .

private:
        void valueChanged(int inValue) { *_modelField = inValue; }
};


Казалось бы, на этом можно было успокоиться… Но я патологический фанат универсальных решений, поэтому решил сделать макрос, позволяющий создавать костылики для обработки разных событий от Qt-объектов в промышленных масштабах.

Властелин костылей

Генератор костыликов

Казалось бы, новый макрос можно сделать на основе старого, просто заменив некоторые идентификаторы на макросные аргументы и слегка обобщив сам макрос.

Макрос для генерации костыликов. Версия 1.0
Опускаю здесь косые чёрточки (вот такие:»\») — бесят жутко!
define QT_EVENT_WORKAROUND_1_ARG(M_WorkaroundName, M_EventName, M_Arg0Type)
class M_WorkaroundName : public QObject {
        Q_OBJECT
public:
        typedef fastdelegate::FastDelegate1 < M_Arg0Type > Callback;

private:
        Callback _callback;

public:
        M_WorkaroundName() : _callback() { }

        void bind(QObject *inQSignalSource, const Callback &inCallback) {
                _callback = inCallback;
                QObject::connect(inQSignalSource, SIGNAL(M_EventName(M_Arg0Type)),
                                this, SLOT(M_EventName(M_Arg0Type));
        }

private slots:
        void M_EventName(M_Arg0Type inValue) { _callback(inValue); }
};


Написав этот макрос, я подумал с уверенностью, что вот теперь уж я точно молодец и вообще властелин костылей (как тот мужик выше). Очень довольный, запустил код и… Да, конечно, ничего не работало. Ошибок компиляции не было, всё собиралось, но коллбек не вызывался, а в лог сыпались сообщения о том, что, дескать, у моего класса TestWorkaround нету нужного слота.

Пришлось копать дальше. Выяснилось, что MOC в Qt не умеет разворачивать макросы. Он проходит по коду до выполнения препроцессора (то бишь, не по тому коду, который, например, можно видеть если выполнить сборку с флагом -E в MinGW, а по никак не обработанному коду).
С другой стороны, MOC должен знать сигнатуры методов, расположенные в блоках декларации класса после слова «slots» — он их читает как строки и дальше использует эти строковые имена при вызовах QObject: connect (макросы SLOT и SIGNAL извлекают эти имена + немного метаданных про место использования). Таким образом, стало ясно, что от пользователя макроса для генерации костыликов придётся требовать-таки писать свою реализацию слота.

Я постарался минимизировать объём и сложность этого кода и окончательное решение выглядит так (уже финальный код, с богомерзкими косыми чёрточками, ага):

#define SIGNAL_WORKAROUND_1_ARG(M_WorkaroundName, M_CallName, M_Arg0Type)\
class M_WorkaroundName : public QObject {\
public:\
        typedef fastdelegate::FastDelegate1< M_Arg0Type > Delegate;\
\
private:\
        Delegate _delegate;\
\
public:\
        void bind(QObject *inQSignalSource, const Delegate &inDelegate) {\
                _delegate = inDelegate;\
                QObject::connect(inQSignalSource, SIGNAL(M_CallName(M_Arg0Type)),\
                                this, SLOT(M_CallName(M_Arg0Type)));\
        }\
\
        void CALL(M_Arg0Type inArgument) { _delegate(inArgument); }\

 — Как видите, макрос описывает не до конца определённый класс, закончить который пользователю нужно описанием слота для вызова метода CALL (…). Полная инструкция по использованию генератора костыликов ниже…

Полная инструкция:
1. Сгенерировать где-нибудь класс-костылик с использованием макроса. В примере мы будем генерировать костылик с именем YourWorkaroundName, который оборачивает событие qtEventName, принимающее один аргумент типа EventArg1Type). Код для генерации класса:

SIGNAL_WORKAROUND_1_ARG(YourWorkaroundName, qtEventName, EventArg1Type)
        Q_OBJECT private slots: void qtEventName(EventArg1Type a0) { CALL(a0); }
};

2. Использовать новый тип в любом месте кода, где нужно обрабатывать события от каких-либо объектов Qt, которые умеют отсылать событие оборачиваемого типа (в примере — событие qtEventName, отправляющее один аргумент типа EventArg1Type). Пример кода, использующего костылик:

class UserClass {
private:
        QSomeObject *_qTestObject;
        YourWorkaroundName _workaround;

public:
        UsingClass() : _qTestObject(new QSomeObject()) {
                _workaround.bind(_qTestObject,
                                YourWorkaroundName::Callback(&UsingClass:: onEvent));
        }

        void onEvent(EventArg1Type inArg) { /* Some actions on callback */ }
}

Всё, готово. Теперь вы можете обрабатывать сообщения от Qt-объектов в любых классах, без ограничений, накладываемых Qt MOC.

В заключение — несколько замечаний:
1. Предложенный макрос годится для событий, принимающих один аргумент на вход. Для обработки другого количества аргументов можно либо сделать копи-пасту этого макроса (олдфаг-стайл), либо использовать variadic macro из С++11.
2. Предложенное решение использует библиотеку FastDelegate для работы с коллбеками. Вы можете легко заменить FastDelefate в макросе на свой тип, если хотите использовать свои функторы.
3. В данном решении нет обработки ошибок, ассертов, и.т.д — в моём случае подобная обработка не требуется. Можете добавить по вкусу.
4. Я готов согласиться с тем, что предложенное решение это адово адище и с удовольствием выслушаю предложения как ещё можно было справиться с ограничениями Qt MOC. Добавлю ваши предложения в статью с указанием авторства решения и с благодарностью от себя лично. Заранее спасибо!

Заключение

Надеюсь, предложенный генератор костыликов поможет сэкономить кому-нибудь время на написание аналогичной жести. Напоследок замечу ещё, что генератор костыликов содержит небольшое количество кода, поэтому, как по мне, нет смысла выкладывать его на GitHub.
Однако, по пожеланию почтенной публики могу сделать маленькую репу с тест-драйвом. Высказывайтесь в комментах по поводу, если желающих будет больше пяти — выложу.

Спасибо за внимание и за то, что дочитали!

П.С.: Если будете находить какие-нибудь ошибки в статье — пишите, буду править.

© Habrahabr.ru