Преамбула


В процессе разработке ПО у меня возникла необходимость определения перечислителей 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...
— Погоди, что? Нужно делать это в классе?.. А я хотел глобально и для всех…

Возможности


  1. Если нам достаточно просто хранить экземпляры нашего enum в QVariant, то нам хватит макроса Q_DECLARE_METATYPE(). Передавать значение через сигналы-слоты, хранить в свойствах можно и в int, а по мере надобности приводить его явно к нашему enum.

  2. Если нам очень важно различать просто int от нашего enum, или хотим иметь пачку invokable методов с одним именем, но различием по типу аргумента, то уже не обойтись без дополнительной регистрации в системе мета-типов. Можно делать это вызовом qRegisterMetaType<T>() где-то до фактического использования.

  3. Ну а если 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)


  1. Zifix
    05.11.2016 17:10
    -6

    Статья полезная, но форматирование с цитатами выглядит очень громоздко и затрудняет чтение. Нужно больше воздуха.


  1. iCpu
    07.11.2016 10:32
    -1

    Диалог с Qt5:
    — Эй, Qt, а можешь мне мозги не парить?
    > В смысле?
    — Давай без этих твоих заумных метатипов.
    > Вы не желаете вызывать всё через безопасные преобразования в строку?
    — Ага, вообще не вариант.
    > Можно вызывать по новому синтаксису, передавая указатели на сигналы классов, унаследованных от QObject.
    > connect(&m1,&myClass::mySignal,&m2,&myClass::mySlot);
    — Точно не надо лишних движений?
    > Абсолютно. Ничего регистрировать не нужно, типы будут проверяться из типов указателей.
    — Должен же быть подвох?
    > Слоты будут вызываться точно по синтаксису. Это означает, что потокобезопасность вызовов не гарантирована.
    — Ясно. Какие ещё плюшки есть?
    > В качестве слотов можно использовать любые методы, в том числе и произвольных классов, не унаследованных от QObject, и даже функции подходящего синтаксиса.
    — Погоди, функции?
    > Точно так. Функции. Синтаксис connect(&m3,&myClass::mySignal,[](){qDebug() << «AHOY!»;});
    — …
    — …
    > Что-то не так?
    — …
    — А раньше нельзя было сказать?
    > ¬(???`?)-