Почему Qt использует moc для сигналов и слотов?
Шаблоны — встроенный механизм в C++, позволяющий компилятору генерировать код на лету в зависимости от типа переданных аргументов. Поэтому шаблоны представляют большой интерес для создателей фреймворков, и мы используем продвинутые шаблоны во многих местах в Qt. Однако существуют ограничения: есть вещи, которые легко выразить с помощью шаблонов, и есть вещи, которые невозможно выразить с помощью шаблонов. Класс контейнера векторных данных легко выразить, даже с частичной специализацией для типов указателей, в то время как функция, которая устанавливает графический пользовательский интерфейс на основе XML-описания, заданного в виде строки, не выразима как шаблон. А затем есть серая зона между ними. Вещи, которые можно реализовать с помощью шаблонов ценой увеличения размера кода, снижения читабельности, портативности, удобства использования, расширяемости, надежности и в конечном итоге красоты дизайна. И шаблоны, и препроцессор C могут быть расширены для выполнения невероятно умных и умопомрачительных вещей. Но просто потому, что эти вещи можно сделать, это не обязательно означает, что их реализация является правильным выбором дизайна. К сожалению, код предназначен не для публикации в книгах, а для компиляции с помощью реальных компиляторов на реальных операционных системах.
Вот несколько причин, почему Qt использует moc:
Синтаксис имеет значение
Синтаксис — это не просто синтаксический сахар: синтаксис, который мы используем для выражения наших алгоритмов, может значительно повлиять на удобочитаемость и поддерживаемость нашего кода. Синтаксис, используемый для сигналов и слотов Qt, доказал свою эффективность на практике. Синтаксис интуитивно понятен, прост в использовании и легко читается. Люди, изучающие Qt, обнаруживают, что синтаксис помогает им понять и использовать концепцию сигналов и слотов — несмотря на ее высокую абстрактность и общность. Это помогает программистам правильно спроектировать свои приложения с самого начала, даже не задумываясь о шаблонах проектирования.
Генераторы кода хороши
Qt's moc (Meta Object Compiler) предоставляет чистый способ выйти за пределы возможностей языка компилирования. Он делает это, генерируя дополнительный код C++, который может быть скомпилирован любым стандартным компилятором C++. moc считывает файлы исходного кода C++. Если он находит одно или несколько объявлений классов, содержащих макрос Q_OBJECT, он генерирует другой файл исходного кода C++, который содержит код метаобъекта для этих классов. Файл исходного кода C++, сгенерированный moc, должен быть скомпилирован и связан с реализацией класса (или может быть #included в файл исходного кода класса). Обычно moc не вызывается вручную, а автоматически системой сборки, поэтому он не требует дополнительных усилий от программиста.
moc не единственный генератор кода, используемый Qt. Другой яркий пример — uic (Компилятор пользовательского интерфейса). Он принимает описание пользовательского интерфейса в формате XML и создает код C++, который настраивает форму. Вне Qt генераторы кода также распространены. Например, rpc и idl, которые позволяют программам или объектам взаимодействовать через границы процессов или машин. Или множество генераторов сканеров и парсеров, где lex и yacc являются наиболее известными. Они принимают описание грамматики в качестве входных данных и генерируют код, реализующий конечный автомат. Альтернатива генераторам кода — это модифицированные компиляторы, проприетарные языки или инструменты графического программирования с односторонними диалогами или мастерами, которые генерируют запутанный код во время проектирования, а не компиляции. Вместо того, чтобы привязывать наших клиентов к проприетарному компилятору C++ или к определенной интегрированной среде разработки, мы предоставляем им возможность использовать любые предпочитаемые инструменты. Вместо того, чтобы заставлять программистов добавлять сгенерированный код в репозитории исходного кода, мы рекомендуем им добавлять наши инструменты в свою систему сборки: чище, безопаснее и более в духе UNIX.
GUI динамичны
C++ — это стандартизированный, мощный и сложный язык общего назначения. Это единственный язык, который используется во столь широком спектре программных проектов, охватывающих все типы приложений, от целых операционных систем, серверов баз данных и высокопроизводительных графических приложений до обычных настольных приложений. Одним из ключей к успеху C++ является масштабируемый дизайн языка, который фокусируется на максимальной производительности и минимальном потреблении памяти при одновременном сохранении совместимости с ANSI C.
Несмотря на все эти преимущества, есть и некоторые недостатки. Для C++ статическая модель объектов является очевидным недостатком по сравнению с динамическим подходом к обмену сообщениями в Objective C, когда речь идет о программировании графического пользовательского интерфейса на основе компонентов. То, что хорошо подходит для высокопроизводительного сервера базы данных или операционной системы, необязательно является правильным выбором дизайна для графического пользовательского интерфейса. С помощью moc, мы превратили этот недостаток в преимущество и добавили гибкость, необходимую для решения задачи безопасного и эффективного программирования графического пользовательского интерфейса.
Наш подход выходит далеко за рамки того, что можно сделать с шаблонами. Например, у нас могут быть свойства объектов. И у нас могут быть перегруженные сигналы и слоты, что кажется естественным при программировании на языке, где перегрузка является ключевой концепцией. Наши сигналы не добавляют нулевых байтов к размеру экземпляра класса, что означает, что мы можем добавлять новые сигналы, не нарушая двоичную совместимость.
Еще одно преимущество заключается в том, что мы можем исследовать сигналы и слоты объекта во время выполнения. Мы можем устанавливать соединения с помощью безопасного по типам вызова по имени, не зная точных типов объектов, которые мы соединяем. Это невозможно с помощью шаблонооснованного решения. Этот вид интроспекции во время выполнения открывает новые возможности, например, GUI, которые генерируются и соединяются из XML-файлов пользовательского интерфейса Qt Designer.
Производительность вызовов не все
Реализация сигналов и слотов Qt не так быстра, как решение на основе шаблонов. В то время как отправка сигнала примерно соответствует затратам на четыре обычных вызова функции с общими реализациями шаблонов, Qt требует усилий, сопоставимых примерно с десятью вызовами функции. Это неудивительно, так как механизм Qt включает в себя универсальный маршализер, интроспекцию, вызовы в очереди между различными потоками и, в конечном итоге, возможность работы со скриптами. Он не полагается на чрезмерную встраиваемость и расширение кода, и он обеспечивает непревзойденную безопасность во время выполнения. Итераторы Qt безопасны, в то время как итераторы более быстрых систем, основанных на шаблонах, таковыми не являются. Даже во время отправки сигнала нескольким получателям эти получатели могут быть удалены безопасно, не вызывая сбоя вашей программы. Без этой безопасности ваша программа в конечном итоге вызовет сбой с ошибкой чтения или записи освобожденной памяти, которую трудно отладить.
Тем не менее, разве решение на основе шаблонов не может улучшить производительность приложения, использующего сигналы и слоты? Хотя Qt добавляет небольшую нагрузку к затратам на вызов слота через сигнал, стоимость вызова — лишь небольшая часть общей стоимости слота. Обычно бенчмаркинг системы сигналов и слотов Qt выполняется с пустыми слотами. Как только вы выполняете какие-либо полезные действия в своих слотах, например, несколько простых строковых операций, накладные расходы на вызовы становятся незначительными. Система Qt настолько оптимизирована, что всё, что требует operator new или delete (например, операции со строками или вставка/удаление чего-либо из контейнера шаблонов), значительно дороже, чем отправка сигнала.
Примечание: Если у вас есть соединение сигналов и слотов в узком внутреннем цикле критически важной для производительности задачи, и вы определяете это соединение как узкое место, подумайте о использовании стандартного шаблона интерфейса слушателя вместо сигналов и слотов. В случаях, когда это происходит, вам, вероятно, нужен только 1:1 соединение. Например, если у вас есть объект, который загружает данные из сети, это совершенно разумный дизайн, если использовать сигнал для указания того, что запрошенные данные прибыли. Но если вам нужно отправлять каждый байт по одному потребителю, используйте интерфейс слушателя, а не сигналы и слоты.
Без ограничений
Благодаря наличию moc для сигналов и слотов, мы смогли добавить к ним другие полезные вещи, которые невозможно сделать с помощью шаблонов. К ним относятся ограниченные переводы через сгенерированную tr() функцию и расширенная система свойств с интроспекцией и расширенной информацией о типе во время выполнения. Одна только система свойств — это большое преимущество: создание мощного и универсального инструмента разработки пользовательского интерфейса, такого как Qt Designer, было бы гораздо сложнее, если не невозможно, без мощной и интроспективной системы свойств. Но на этом всё не заканчивается. Мы также предоставляем динамический механизм qobject_cast<T>(), который не зависит от RTTI системы, и поэтому не имеет его ограничений. Мы используем его для безопасного запроса интерфейсов из динамически загруженных компонентов. Другая область применения — это динамические метаобъекты. Например, мы можем взять ActiveX-компоненты и во время выполнения создать вокруг них метаобъект. Или мы можем экспортировать компоненты Qt в виде ActiveX-компонентов, экспортируя его метаобъект. Вы не можете сделать ни то, ни другое с помощью шаблонов.
C++ с moc в значительной степени предоставляет нам гибкость Objective-C или Java Runtime Environment, сохраняя при этом уникальные преимущества C++ в плане производительности и масштабируемости. Именно это делает Qt таким гибким и удобным инструментом, который мы имеем сегодня.
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/archives/qt-5.11/why-moc.html