[Перевод] Развенчание мифов о мета-объектном компиляторе Qt

Я часто встречаю критику фреймворка Qt, в которой ему пеняют использованием мета-объектного компилятора (утилиты moc). Как один из разработчиков moc, я решил написать данную статью с целью развенчать некоторые связанные с этим мифы.

Вступление


Moc — это один из инструментов разработчика и часть библиотеки Qt. Его задача — поддерживать расширение языка С++, необходимое для интроспекции и рефлексии в Qt (сюда относятся сигналы, слоты и QML). Для более детального объяснение вы можете почитать о том, как работают сигналы и слоты в Qt.

Необходимость использования moc является одним из главных объектов критики Qt. Это даже привело к появлению форков Qt, принципиально отказавшихся от moc (например, CopperSpice). Но всё-же большинство приписываемых moc так называемых недостатков не обоснованы.

Мифы


Moc переписывает ваш код перед тем, как передать его компилятору


Это распространённое заблуждение. Moc не модифицирует и не переписывает ваш код. Он просто парсит часть кода для того, чтобы сгенерировать дополнительные С++ файлы, которые потом будут компилироваться независимо. Это не очень большое отличие, но всё-же важное техническое недопонимание.

Moc просто автоматически генерирует шаблонный код, который можно было бы долго и нудно писать вручную, если бы moc не существовало. Если вы мазохист, то и сейчас вполне возможно взять и написать самостоятельно все таблицы для интроспекции и реализации сигналов. Ну или положиться на надёжный инструмент, который сделает это за вас.

Используя Qt, вы не пишете на настоящем С++


Я слышал этот довод много раз, но он попросту неверен. Макросы, используемые moc для аннотации кода — это стандартные макросы С++. Они должны быть корректно распознаны любым инструментом, способным анализировать код на С++. Когда вы добавляете в код Q_OBJECT, то просто дописываете объявление нескольких функций. Когда вы пишете «signals:», то просто добавляете макрос, который превратится в «public:». Многие другие макросы Qt вообще ни во что не раскрываются. Moc просто находит их и генерирует код эмиттеров сигналов и таблиц интроспекции.

Тот факт, что ваш код теперь может быть прочитан ещё одним инструментом, не делает его «менее соответствующим стандарту С++». Вы же не считаете код, написанный с расчётом на использование gettext или doxygen каким-то «менее правильным С++»?

Moc усложняет процесс сборки кода


Если вы используете любую промышленную систему сборки кода, вроде CMake или qmake, то получаете нативную поддержку Qt. Даже с какой-то собственной системой сборки речь идёт о всего-лишь одном дополнительном запуске команды обработки заголовочных файлов. Все известные мне системы сборки позволяют добавлять шаги по запуску дополнительных команд перед запуском компилятора, поскольку многие проекты в том или ином виде используют генерацию кода при сборке проекта. Вспомните, например, инструменты вроде yacc/bison, gperf, llvm/TableGen.

Moc делает отладку сложнее


Поскольку moc генерирует код на чистом С++, то отладчики не должны иметь никаких проблем с ним. Мы стараемся поддерживать сгенерированный код в таком состоянии, чтобы он не вызывал предупреждений компиляторов или инструментов статического или динамического анализа кода. Да, иногда при отладке вы будете видеть в колстеке следы сгенерированного moc кода. В некоторых редких случаях вы можете получить ошибку, связанную с кодом, созданным moc, но обычно причины достаточно легко обнаружить. Код, сгенерированный moc, достаточно человекочитаем. Также его, вероятно, понимать и отлаживать даже проще, чем те печально известные ошибки некоторых библиотек, построенных на продвинутом использовании шаблонов.

Отказ от moc улучшает производительность на этапе выполнения кода


Это прямая цитата с главной страницы CopperSpice, и, вероятно, самая большая их ложь. Код, генерируемый moc, очень тщательно старается избегать динамических аллокаций и уменьшить использование реаллокаций памяти. Генерируемые moc таблицы являют собой константные массивы и хранятся в read-only сегменте данных. CopperSpice же регистрирует свои QMetaObject (информацию о сигналах, слотах и свойствах) на рантайме.

Milian Wolff проделал некоторые сравнения производительности Qt и CopperSpice в его докладе на CppCon2015. Вот скриншот одного из его слайдов (меньше — лучше).

image

Также нужно отметить, что код на Qt даже с учётом запуска moc компилируется быстрее, чем код на CopperSpice.

Устаревшие мифы


Некоторая критика когда-то была справедливой, но более таковой не является.

Макрос не может быть использован при объявлении сигнала, слота, базового класса объекта, а также …


До выхода Qt5 утилита moc действительно не раскрывала макросы. Но начиная с Qt 5.0 moc полностью поддерживает макросы во всех вышеперечисленных местах, так что это больше совершенно не является проблемой.

Перечисления (enums) и переопределения типов (typedefs) должны строго соответствовать при использовании их в качестве параметров сигналов и слотов


Это является проблемой только если вы всё ещё хотите использовать синтаксис соединений, основанный на строках (поскольку там действительно используется прямое сравнение названий типов). С выходом Qt5 и новым синтаксисом это больше не препятствие.

Q_PROPERTY не позволяет использовать запятые в типах


Q_PROPERTY это макрос с одним аргументом, который ни во что не раскрывается и служит лишь для помощи moc. Но, поскольку это всё ещё макрос, запятая в, например, QMap разделяет аргументы макроса и вызывает ошибку компиляции. Когда я увидел, как CopperSpice использует этот аргумент против Qt, то потратил 5 минут на то, чтобы исправить это с использованием variadic-макросов из стандарта С++11.

Другая критика

Шаблоны, вложенные классы или классы, используемые множественное наследование, не могут быть QObject-ами


Хотя это и является правдой, но эти возможности просто пока не поддерживаются QObject, хотя и вполне могут быть реализованы в moc, если мы этого захотим. Проект Qt в данный момент не считает данные фичи приоритетными.

Я однажды добавил поддержку шаблонных QObjects в moc, но это изменение не вошло в основную ветку разработки, поскольку никто больше не выразил интереса к данной функциональности.

Ещё нужно отметить поддержку шаблонных и вложенных классов в moc-ng.

Множественное наследование уже само по себе является очень неоднозначным. Чаще всего оно указывает на проблемы с архитектурой приложения и многие современные языки напрямую его запрещают. Вы всё ещё можете использовать множественное наследование с Qt, если если QObject является первым базовым классом в цепочке наследования. Это небольшое ограничение позволяет нам применять полезные оптимизации. Когда-нибудь задумывались почему qobject_cast настолько быстрее, чем dynamic_cast?

Выводы


Я не думаю, что moc — это проблема. Макросы Qt действительно помогают реализовать необходимую Qt функциональность. Сравнивая их с подходом CopperSpice мы можем заметить в последнем значительную избыточность служебного кода, а также недружелюбный синтаксис макросов (не говоря уже о потерях производительности на рантайме). Синтаксис сигналов и слотов, который существует в Qt с 90-ых годов — одна из фундаментальных вещей, обеспечивших успех фреймворка.

Вам может быть также интересно изучить некоторые эксперименты, связанные с moc, вроде moc-ng (это moc, переписанный с использованием библиотек clang). Также есть вот это исследование замены moc с помощью инструментов рефлексии С++. Ну и библиотека Verdigris, с макросами, создающими QMetaObject без moc.

Комментарии (1)

  • 24 апреля 2017 в 13:46

    0

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

    Другое дело, что полностью отказываться от него никто не будет в здравом уме…

© Habrahabr.ru