Преамбула
В процессе разработке ПО у меня возникла необходимость определения перечислителей enum в централизованном заголовочном файле, тогда как их использование могло быть во многих исходных файлах. Это весьма удобно с точки зрения организации исходников и зависимостей. Однако для моих задач также требовалась регистрация перечислителей в системе метатипов Qt. О том, как делал я такую регистрацию и пойдет речь.
Интервью с Qt
— Мне нужно чтобы ты понимал мой enum.
— Для объявления enum в качестве мета-типа для QVariant подойдет Q_DECLARE_METATYPE(). Макрос следует вызывать сразу после объявления enum.— Отлично! А если я хочу кидаться своим enum от сигналов к слотам, да в свойства пихать?
— Для регистрации enum в системе мета-типов потребуется qRegisterMetaType<T>(). Обратите внимание, это функция. Ее надо где-то вызвать. Желательно, до любого швыряния и пихания. В main(), например. И, да, без Q_DECLARE_METATYPE() ничего не получится.— Хм… Это не удобно. Хочу чтобы вся регистрация ограничивалась одним местом в исходниках…
— Что же вы сразу не сказали?! Используйте Q_ENUM()! Этот замечательный макрос все сделает за вас! И даже больше! Он пропишет конверсию из вашего enum в QString для QVariant! Вам всего лишь надо вызвать макрос сразу после объявления enum внутри определения класса, наследованного от QObject...— Погоди, что? Нужно делать это в классе?.. А я хотел глобально и для всех…
Возможности
- Если нам достаточно просто хранить экземпляры нашего enum в QVariant, то нам хватит макроса Q_DECLARE_METATYPE(). Передавать значение через сигналы-слоты, хранить в свойствах можно и в int, а по мере надобности приводить его явно к нашему enum.
- Если нам очень важно различать просто int от нашего enum, или хотим иметь пачку invokable методов с одним именем, но различием по типу аргумента, то уже не обойтись без дополнительной регистрации в системе мета-типов. Можно делать это вызовом qRegisterMetaType<T>() где-то до фактического использования.
- Ну а если enum является частью некоторого QObject-класса, то все решается Q_ENUM(), после этого с ним можно делать что угодно. Даже использовать в кач-ве свойства или параметра слота в другом классе через полное имя типа (ClassName::EnumName).
Решение
Моя целевая задача, сформулированная ранее, требовала полноценной регистрации в системе мета-типов. Мне подходили две последних возможности. У возможности номер 2 недостаток в необходимости регистрировать enum явным вызовом функции где-то в коде. У возможности номер 3 недостаток в потенциальном усложнении зависимостей исходников, если размещать enum внутрь одного из имеющихся классов, работающих с ним.
Но никто же не мешает нам создать новый QObject-класс! В общем для всех заголовочном файле. И можно запретить инстанцирование этого класса. Приведу пример (а для разминки, в нем же небольшой пример возможностей стандарта c++11):
/// Набор глобальных перечислителей
class Enums : public QObject {
public:
/// достуно с std=с++11, 'class' прячет имена значений внутри имени типа EnumA, ':int' фиксирует размер на указанном int
enum class EnumA: int {
A,
B,
C,
};
Q_ENUM(EnumA)
enum EnumB {
A,///< коллизий с EnumA::A не будет, EnumA::A не доступно как A, в отличие от EnumB::A
D,
};
Q_ENUM(EnumB)
Q_OBJECT
Enums() = delete; ///< std=c++11, обеспечивает запрет на создание любого экземпляра Enums
};
В результате мы имеем класс Enums доступный всем желающим, имеющий нужные enum, известные системе мета-типов, и не имеющий возможность создавать экземпляры себя!
Однако он имеет некоторые минусы. Макрос Q_OBJECT создает, кроме статического мета-объекта, пачку ни разу не статических функций. А наш класс ни разу не будет создаваться! И эти функции никогда не понадобятся. Увы, это некоторая трата неизбежна. Однако, начиная с версии Qt 5.5, в документации описан макрос Q_GADGET.
— Да, да, он легче, чем Q_OBJECT, и не требует наследования от QObject. Разрешает только Q_ENUM, Q_PROPERTY, Q_INVOKABLE. И давно он уже существует, мы просто молчали о нем!
Действительно, что же вы молчали! Вот тут-то он нам отлично подойдет! Всего пара изменений, и штаны превращаются в…
/// Набор глобальных перечислителей
class Enums { /// < Обратите внимание, класс Enums теперь сам по себе!
public:
/// достуно с std=с++11, 'class' прячет имена значений внутри имени типа EnumA, ':int' фиксирует размер на указанном int
enum class EnumA: int {
A,
B,
C,
};
Q_ENUM(EnumA)
enum EnumB {
A,///< коллизий с EnumA::A не будет, EnumA::A не доступно как A, в отличие от EnumB::A
D,
};
Q_ENUM(EnumB)
Q_GADGET ///< Он легче Q_OBJECT и вообще скромняшка!
Enums() = delete; ///< std=c++11, обеспечивает запрет на создание любого экземпляра Enums
};
Нам, правда, не нужна возможность объявления свойств и invokable-методов. Но они неотъемлемая часть мета-объектов, отрезать не получится. Зато теперь, если сравнить старый и новый moc*.cpp на этот заголовочный файл, можно обнаружить пропажу реализации статической ф-ии qt_static_metacall() и обычных ф-ий metaObject(), qt_metacast(), qt_metacall(). Мелочь, а приятно. Любопытства ради, сравнил размеры бинарников, мелочь оказалась 512 байт (сборка Выпуск).
Заключение
Можно весьма легко определить enum в одном заголовочном файле, там же и закинув его в систему мета-типов, и далее использовать его в других классах. К примеру, у меня возникала задача использовать такой enum в некоторой основной логике системы, при этом предоставить возможность выбирать значения этого enum через интерфейс. Дополнительная фича Q_ENUM в виде регистрации имен значений enum позволила не изобретать велосипед, но это уже другая история. С моделями и шаблонами.
Комментарии (2)
iCpu
07.11.2016 10:32-1Диалог с Qt5:
— Эй, Qt, а можешь мне мозги не парить?
> В смысле?
— Давай без этих твоих заумных метатипов.
> Вы не желаете вызывать всё через безопасные преобразования в строку?
— Ага, вообще не вариант.
> Можно вызывать по новому синтаксису, передавая указатели на сигналы классов, унаследованных от QObject.
> connect(&m1,&myClass::mySignal,&m2,&myClass::mySlot);
— Точно не надо лишних движений?
> Абсолютно. Ничего регистрировать не нужно, типы будут проверяться из типов указателей.
— Должен же быть подвох?
> Слоты будут вызываться точно по синтаксису. Это означает, что потокобезопасность вызовов не гарантирована.
— Ясно. Какие ещё плюшки есть?
> В качестве слотов можно использовать любые методы, в том числе и произвольных классов, не унаследованных от QObject, и даже функции подходящего синтаксиса.
— Погоди, функции?
> Точно так. Функции. Синтаксис connect(&m3,&myClass::mySignal,[](){qDebug() << «AHOY!»;});
— …
— …
> Что-то не так?
— …
— А раньше нельзя было сказать?
> ¬(???`?)-
Zifix
Статья полезная, но форматирование с цитатами выглядит очень громоздко и затрудняет чтение. Нужно больше воздуха.