Qt хорошо известен своим механизмом сигналов и слотов. Но как это работает? В этом посте мы исследуем внутренности
QObjectи
QMetaObjectи раскроем их работу за кадром. Я буду давать примеры Qt5 кода, иногда отредактированные для краткости и добавления форматирования.
Сигналы и слоты
Для начала, вспомним, как выглядят сигналы и слоты, заглянув в официальный пример. Заголовочный файл выглядит так:
Где-то, в .cpp файле, мы реализуем
setValue():
Затем, можем использовать объект Counter таким образом:
Это оригинальный синаксис, который почти не изменялся с начала Qt в 1992 году. Но даже если базовое API не было изменено, реализация же менялась несколько раз. Под капотом добавлялись новые возможности и происходили другие вещи. Тут нет никакой магии и я покажу как это работает.
MOC или метаобъектный компилятор
Сигналы и слоты, а также система свойств Qt, основываются на возможностях самоанализа объектов во время выполнения программы. Самоанализ означает способность перечислить методы и свойства объекта и иметь всю информацию про них, в частности, о типах их аргументов.
QtScriptи
QMLвряд ли был бы возможны без этого.
C++ не предоставляет родной поддержки самоанализа, поэтому Qt поставляется с инструментом, который это обеспечивает. Этот инструмент называется
MOC. Это кодогенератор (но не препроцессор, как думают некоторые люди).
Он парсит заголовочные файлы и генерирует дополнительный C++ файл, который компилируется с остальной частью программы. Этот сгенерированный C++ файл содержит всю информацию, необходимую для самоанализа.
Qt иногда подвергается критике со стороны языковых пуристов, так как это дополнительный генератор кода. Я позволю документации Qt ответитьна эту критику. Нет ничего плохого в кодогенераторе и
MOCявляется превосходным помощником.
Магические макросы
Сможете ли вы заметить ключевые слова, которые не являются ключевыми словами C++?
signals,
slots,
Q_OBJECT,
emit,
SIGNAL,
SLOT. Они известны как Qt-расширение для C++. На самом деле это простые макросы, которые определены в qobjectdefs.h.
Это правда, сигналы и слоты являются простыми функциями: компилятор обрабатывает их как и любые другие функции. Макросы еще служат определённой цели:
MOCвидит их. Сигналы были в секции protected в Qt4 и ранее. Но в Qt5 они уже открыты, для поддержки нового синтаксиса.
Q_OBJECTопределяет связку функций и статический
QMetaObject. Эти функции реализованы в файле, сгенерированном
MOC.
emit– пустой макрос. Он даже не парсится
MOC. Другими словами,
emitопционален и ничего не значит (за исключением подсказки для разработчика).
Эти макросы просто используются препроцессором для конвертации параметра в строку и добавления кода в начале. В режиме отладки мы также дополняем строку с расположением файла предупреждением, если соединение с сигналом не работает. Это было добавлено в Qt 4.5 для совместимости. Для того, чтобы узнать, какие строки содержат информацию о строке, мы используем
qFlagLocation, которая регистрирует адрес строки в таблице, с двумя включениями.
Теперь перейдём к коду, сгенерированному
MOC.
QMetaObject
Тут мы видим реализацию
Counter::metaObject()и
Counter::staticMetaObject. Они объявленный в макросе
Q_OBJECT. QObject::d_ptr->metaObject используется только для динамических метаобъектов (
QMLобъекты), поэтому, в общем случае, виртуальная функция
metaObject()просто возвращает staticMetaObject класса. staticMetaObject построен с данными только для чтения.
QMetaObjectопределён в qobjectdefs.h в виде:
d косвенно символизирует, что все члены должны быть сокрыты, но они не сокрыты для сохранение POD и возможности статической инициализации.
QMetaObjectинициализируется с помощью метаобъекта родительского класса superdata (QObject::staticMetaObject в данном случае). stringdata и data инициализируются некоторыми данными, которые будут рассмотрены далее. static_metacall это указатель на функцию, инициализируемый Counter::qt_static_metacall.
Таблицы самоанализа
Во-первых, давайте посмотрим на основные данные
QMetaObject.
Первые 13 int составляют заголовок. Он предоставляет собой две колонки, первая колонка – это количество, а вторая – индекс массива, где начинается описание. В текущем случае мы имеем два метода, и описание методов начинается с индекса 14.
Описание метода состоит из 5 int. Первый – это имя, индекс в таблице строк (мы детально рассмотрим её позднее). Второе целое – количество параметров, вслед за которым идёт индекс, где мы может найти их описание. Сейчас мы будет игнорировать тег и флаги. Для каждой функции
MOCтакже сохраняет возвращаемый тип каждого параметра, их тип и индекс имени.
Таблица строк
В основном, это статический массив QByteArray (создаваемый макросом
QT_MOC_LITERAL), который ссылается на конкретный индекс в строке ниже.
Сигналы
MOCтакже реализует сигналы. Они являются функциями, которые просто создают массив указателей на аргументы и передают их
QMetaObject::activate. Первый элемент массива это возвращаемое значение. В нашем примере это 0, потому что возвращаемое значение void. Третий аргумент, передаваемый функции для активации, это индекс сигнала (0 в данном случае).
Вызов слота
Также возможно вызвать слот по его индексу, используя функцию qt_static_metacall:
Массив указателей на аргументы в таком же формате, как и в сигналах. _a[0] не тронут, потому что везде тут возвращается void.
Примечание по поводу индексов
Для каждого
QMetaObject, сигналам, слотам и прочим вызываемым методам объекта, даются индексы, начинающиеся с 0. Они упорядочены так, что на первом месте сигналы, затем слоты и затем уже прочие методы. Эти индексы внутри называется относительными индексами. Они не включают индексы родителей. Но в общем, мы не хотим знать более глобальный индекс, который не относится к конкретному классу, но включает все прочие методы в цепочке наследования. Поэтому, мы просто добавляем смещение к относительному индексу и получаем абсолютный индекс. Этот индекс, используемый в публичном API, возвращается функциями вида QMetaObject::indexOf{Signal,Slot,Method}.
Механизм соединения использует массив, индексированный для сигналов. Но все слоты занимают место в этом массиве и обычно слотов больше чем сигналов. Так что, с Qt 4.6, появляется новый внутренний индекс для сигналов, который включает только индексы, используемые для сигналов. Если вы разрабатываете с Qt, вам нужно знать только про абсолютный индекс для методов. Но пока вы просматриваете исходный код
QObject, вы должны знать разницу между этими тремя индексами.
Как работает соединение
Первое, что делает Qt при соединении, это ищет индексы сигнала и слота. Qt будет просматривать таблицы строк метаобъекта в поисках соответствующих индексов. Затем, создается и добавляется во внутренние списки объект QObjectPrivate::Connection.
Какая информация необходима для хранения каждого соединения? Нам нужен способ быстрого доступа к соединению для данного индекса сигнала. Так как могут быть несколько слотов, присоединённых к одному и тому же сигналу, нам нужно для каждого сигнала иметь список присоединённых слотов. Каждое соединение должно содержать объект-получатель и индекс слота. Мы также хотим, чтобы соединения автоматически удалялись, при удалении получателя, поэтому каждый объект-получатель должен знать, кто соединён с ним, чтобы он мог удалить соединение.
Вот QObjectPrivate::Connection, определённый в qobject_p.h:
Каждый объект имеет массив соединений: это массив, который связывает каждого сигнала списки QObjectPrivate::Connection. Каждый объект также имеет обратные списки соединений объектов, подключённых для автоматического удаления. Это двусвязный список.
Связные списки используются для возможности быстрого добавления и удаления объектов. Они реализованы с наличием указателей на следующий/предыдущий узел внутри QObjectPrivate::Connection. Заметьте, что указатель prev из senderList это указатель на указатель. Это потому что мы действительно не указываем на предыдущий узел, а, скорее, на следующий, в предыдущем узле. Этот указатель используется только когда соединение разрушается. Это позволяет не иметь специальный случай для первого элемента.
Эмиссия сигнала
Когда мы вызываем сигнал, мы видели, что он вызывает код, сгенерированный
MOC, который уже вызывает
QMetaObject::activate. Вот реализация (с примечаниями) этого метода в qobject.cpp:
UPD: перевод второй части тут.