Почему 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/qt-6.0/why-moc.html