Я часто встречаю критику фреймворка Qt, в которой ему пеняют использованием мета-объектного компилятора (утилиты moc). Как один из разработчиков moc, я решил написать данную статью с целью развенчать некоторые связанные с этим мифы.
Moc — это один из инструментов разработчика и часть библиотеки Qt. Его задача — поддерживать расширение языка С++, необходимое для интроспекции и рефлексии в Qt (сюда относятся сигналы, слоты и QML). Для более детального объяснение вы можете почитать о том, как работают сигналы и слоты в Qt.
Необходимость использования moc является одним из главных объектов критики Qt. Это даже привело к появлению форков Qt, принципиально отказавшихся от moc (например, CopperSpice). Но всё-же большинство приписываемых moc так называемых недостатков не обоснованы.
Это распространённое заблуждение. Moc не модифицирует и не переписывает ваш код. Он просто парсит часть кода для того, чтобы сгенерировать дополнительные С++ файлы, которые потом будут компилироваться независимо. Это не очень большое отличие, но всё-же важное техническое недопонимание.
Moc просто автоматически генерирует шаблонный код, который можно было бы долго и нудно писать вручную, если бы moc не существовало. Если вы мазохист, то и сейчас вполне возможно взять и написать самостоятельно все таблицы для интроспекции и реализации сигналов. Ну или положиться на надёжный инструмент, который сделает это за вас.
Я слышал этот довод много раз, но он попросту неверен. Макросы, используемые moc для аннотации кода — это стандартные макросы С++. Они должны быть корректно распознаны любым инструментом, способным анализировать код на С++. Когда вы добавляете в код Q_OBJECT, то просто дописываете объявление нескольких функций. Когда вы пишете «signals:», то просто добавляете макрос, который превратится в «public:». Многие другие макросы Qt вообще ни во что не раскрываются. Moc просто находит их и генерирует код эмиттеров сигналов и таблиц интроспекции.
Тот факт, что ваш код теперь может быть прочитан ещё одним инструментом, не делает его «менее соответствующим стандарту С++». Вы же не считаете код, написанный с расчётом на использование gettext или doxygen каким-то «менее правильным С++»?
Если вы используете любую промышленную систему сборки кода, вроде CMake или qmake, то получаете нативную поддержку Qt. Даже с какой-то собственной системой сборки речь идёт о всего-лишь одном дополнительном запуске команды обработки заголовочных файлов. Все известные мне системы сборки позволяют добавлять шаги по запуску дополнительных команд перед запуском компилятора, поскольку многие проекты в том или ином виде используют генерацию кода при сборке проекта. Вспомните, например, инструменты вроде yacc/bison, gperf, llvm/TableGen.
Поскольку moc генерирует код на чистом С++, то отладчики не должны иметь никаких проблем с ним. Мы стараемся поддерживать сгенерированный код в таком состоянии, чтобы он не вызывал предупреждений компиляторов или инструментов статического или динамического анализа кода. Да, иногда при отладке вы будете видеть в колстеке следы сгенерированного moc кода. В некоторых редких случаях вы можете получить ошибку, связанную с кодом, созданным moc, но обычно причины достаточно легко обнаружить. Код, сгенерированный moc, достаточно человекочитаем. Также его, вероятно, понимать и отлаживать даже проще, чем те печально известные ошибки некоторых библиотек, построенных на продвинутом использовании шаблонов.
Это прямая цитата с главной страницы CopperSpice, и, вероятно, самая большая их ложь. Код, генерируемый moc, старается избегать динамических аллокаций и уменьшить количество реаллокаций памяти. Генерируемые moc таблицы являют собой константные массивы и хранятся в read-only сегменте данных. CopperSpice же регистрирует свои QMetaObject (информацию о сигналах, слотах и свойствах) на рантайме.
Milian Wolff проделал некоторые сравнения производительности Qt и CopperSpice в его докладе на CppCon2015. Вот скриншот одного из его слайдов (меньше — лучше).

Также нужно отметить, что код на Qt даже с учётом запуска moc компилируется быстрее, чем код на CopperSpice.
Некоторая критика когда-то была справедливой, но более таковой не является.
До выхода Qt5 утилита moc действительно не раскрывала макросы. Но начиная с Qt 5.0 moc полностью поддерживает макросы во всех вышеперечисленных местах, так что это больше совершенно не является проблемой.
Это является проблемой только если вы всё ещё хотите использовать синтаксис соединений, основанный на строках (поскольку там действительно используется прямое сравнение названий типов). С выходом Qt5 и новым синтаксисом это больше не препятствие.
Q_PROPERTY это макрос с одним аргументом, который ни во что не раскрывается и служит лишь для помощи moc. Но, поскольку это всё ещё макрос, запятая в, например, QMap<Foo, Bar> разделяет аргументы макроса и вызывает ошибку компиляции. Когда я увидел, как CopperSpice использует этот аргумент против Qt, то потратил 5 минут на то, чтобы исправить это с использованием variadic-макросов из стандарта С++11.
Хотя это и является правдой, но эти возможности просто пока не поддерживаются 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.
Вступление
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. Вот скриншот одного из его слайдов (меньше — лучше).

Также нужно отметить, что код на Qt даже с учётом запуска moc компилируется быстрее, чем код на CopperSpice.
Устаревшие мифы
Некоторая критика когда-то была справедливой, но более таковой не является.
Макрос не может быть использован при объявлении сигнала, слота, базового класса объекта, а также ...
До выхода Qt5 утилита moc действительно не раскрывала макросы. Но начиная с Qt 5.0 moc полностью поддерживает макросы во всех вышеперечисленных местах, так что это больше совершенно не является проблемой.
Перечисления (enums) и переопределения типов (typedefs) должны строго соответствовать при использовании их в качестве параметров сигналов и слотов
Это является проблемой только если вы всё ещё хотите использовать синтаксис соединений, основанный на строках (поскольку там действительно используется прямое сравнение названий типов). С выходом Qt5 и новым синтаксисом это больше не препятствие.
Q_PROPERTY не позволяет использовать запятые в типах
Q_PROPERTY это макрос с одним аргументом, который ни во что не раскрывается и служит лишь для помощи moc. Но, поскольку это всё ещё макрос, запятая в, например, QMap<Foo, Bar> разделяет аргументы макроса и вызывает ошибку компиляции. Когда я увидел, как 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.