Почему Qt использует moc для сигналов и слотов?
Шаблоны — встроенный механизм в C++, который позволяет компилятору генерировать код на лету, в зависимости от типа переданных аргументов. Таким образом, шаблоны представляют большой интерес для создателей фреймворков, и мы используем продвинутые шаблоны во многих местах в Qt. Однако есть ограничения: существуют вещи, которые легко выразить с помощью шаблонов, и есть вещи, которые невозможно выразить с помощью шаблонов. Класс контейнера vector с обобщенным типом легко выразить, даже с частичной специализацией для указателей, тогда как функция, которая настраивает графический интерфейс пользователя на основе описания 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 (User Interface Compiler). Он принимает описание пользовательского интерфейса на XML и создает код C++, который настраивает форму. Вне Qt генераторы кода также распространены. Например, rpc и idl, которые позволяют программам или объектам взаимодействовать через границы процессов или машин. Или огромное разнообразие генераторов сканеров и парсеров, где lex и yacc являются наиболее известными. Они принимают грамматическое описание в качестве входных данных и генерируют код, реализующий конечный автомат. Альтернатива генераторам кода — это взломанные компиляторы, проприетарные языки или графические инструменты разработки с односторонними диалогами или мастер-помощниками, которые генерируют непонятный код во время проектирования, а не во время компиляции. Вместо того чтобы привязывать наших клиентов к проприетарному компилятору C++ или к определенной интегрированной среде разработки, мы позволяем им использовать любые предпочитаемые ими инструменты. Вместо того чтобы заставлять программистов добавлять сгенерированный код в хранилища исходных кодов, мы поощряем их добавлять наши инструменты в свою систему сборки: чище, безопаснее и более в духе UNIX.
Графические интерфейсы динамичны
C++ — это стандартизированный, мощный и сложный универсальный язык. Это единственный язык, который используется в столь широком диапазоне проектов программного обеспечения, охватывая все типы приложений, от целых операционных систем, баз данных и высокопроизводительных графических приложений до обычных настольных приложений. Одним из ключей к успеху C++ является масштабируемый дизайн языка, который фокусируется на максимальной производительности и минимальном потреблении памяти, сохраняя при этом совместимость с ANSI C.
При всех этих преимуществах есть и некоторые недостатки. Для C++ статическая модель объектов является явным недостатком по сравнению с динамическим подходом к обмену сообщениями Objective C в отношении программирования графических интерфейсов на основе компонентов. То, что хорошо подходит для высокопроизводительного сервера базы данных или операционной системы, не обязательно является правильным выбором дизайна для графического интерфейса пользователя. С помощью moc, мы превратили этот недостаток в преимущество и добавили необходимую гибкость, чтобы справиться с задачей безопасного и эффективного программирования графических интерфейсов пользователя.
Наш подход выходит далеко за рамки возможностей шаблонов. Например, у нас могут быть свойства объектов. И у нас могут быть перегруженные сигналы и слоты, что кажется естественным при программировании на языке, где перегрузки — ключевая концепция. Наши сигналы не добавляют ни одного байта к размеру экземпляра класса, что означает, что мы можем добавлять новые сигналы, не нарушая двоичную совместимость.
Еще одним преимуществом является то, что мы можем исследовать сигналы и слоты объекта во время выполнения. Мы можем устанавливать соединения с использованием безопасного вызова по имени без необходимости знать точные типы объектов, которые мы соединяем. Это невозможно с решением на основе шаблонов. Такой вид интроспекции во время выполнения открывает новые возможности, например, графические интерфейсы, которые генерируются и соединяются из 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/qt-6.2/why-moc.html