Почему 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 UI Qt Designer.
Производительность вызовов — не всё
Реализация сигналов и слотов Qt не так быстра, как шаблоно-ориентированное решение. В то время как отправка сигнала примерно соответствует стоимости четырёх обычных вызовов функций с типичными реализациями шаблонов, Qt требует усилий, сопоставимых с приблизительно десятью вызовами функций. Это неудивительно, так как механизм Qt включает в себя универсальный маршаллинг, интроспекцию, очереди вызовов между различными потоками и, в конечном итоге, возможность работы со сценариями. Он не полагается на чрезмерную вставку и расширение кода и обеспечивает непревзойдённую безопасность во время выполнения. Итераторы Qt безопасны, в то время как итераторы более быстрых шаблоно-ориентированных систем — нет. Даже во время отправки сигнала нескольким получателям, эти получатели могут быть безопасно удалены, не вызывая сбоя вашей программы. Без этой безопасности ваша программа в конечном итоге рухнет из-за ошибки чтения или записи освобождённой памяти, которую трудно отладить.
Тем не менее, не может ли шаблоно-ориентированное решение улучшить производительность приложения, использующего сигналы и слоты? Хотя верно, что Qt добавляет небольшую нагрузку на стоимость вызова слота через сигнал, стоимость вызова составляет лишь небольшую часть всей стоимости слота. Сравнительные тесты системы сигналов и слотов Qt обычно выполняются со слотами без функциональности. Как только вы что-либо делаете в своих слотах, например несколько простых операций со строками, стоимость вызова становится незначительной. Система Qt настолько оптимизирована, что всё, что требует оператора 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-5.14/why-moc.html