Qt є відомим за свій механізм сигнально-слотових з’єднань. Але як він працює? В цій статті ми дослідимо внутрішню структуру QObject
і QMetaObject
і дізнаємось як сигнали і слоти працюють під капотом.
Я буду наводити приклади коду з Qt5, які в деяких випадках є відредагованими і скороченими.
Сигнали і Слоти
Спочатку, давайте пригадаємо як виглядає сигнально-слотове з’єднання на прикладі з офіційної документації.
Хедер-файл виглядає так:
class Counter : public QObject
{
Q_OBJECT
int m_value;
public:
int value() const { return m_value; }
public slots:
void setValue(int value);
signals:
void valueChanged(int newValue);
};
В .cpp
файлі ми напишемо реалізацію для setValue()
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
emit valueChanged(value);
}
}
Тоді можна використати цей Counter так:
Counter a, b;
QObject::connect(&a, SIGNAL(valueChanged(int)), &b, SLOT(setValue(int)));
a.setValue(12); // a.value() == 12, b.value() == 12
Це оригінальний синтаксис, який майже не змінився з початку появи Qt в 1992 році.
Але навіть якщо базовий API не змінився, його реалізація змінювалась декілька разів. Було додано нові “фічі” і багато чого змінилось всередині. В реалізації сигнально-слотового механізму немає нічого надзвичайного і ця стаття покаже вам як він працює.
MOC - Компілятор Мета Об’єктів(Meta Object Compiler)
Сигнально-слотова система та система властивостей (property system) базуються на здатності до інтроспекції об’єкта під час виконання програми. Інтроспекція це можливість зберігати список методів і властивостей об’єкта та мати всю інформацію про них, наприклад тип їх аргументів. Без цього QtScript і QML були б неможливими.
С++ не надає підтримку інтроспекції, тому Qt має інструмент для її забезпечення. Цим інструментом є MOC. Це генератор коду і аж ніяк НЕ препроцесор, як дехто його називає.
Він аналізує хедер файл і генерує додатково C++ файл, який компілюється з всією програмою. Згенерований файл містить всю необхідну інформацію для забезпечення інтроспекції.
Qt часом критикують через цей додатковий генератор коду ті, хто виступає за чистоту мови. Відповідь на критику можна знайти в документації QT. Немає нічого поганого в генераторах коду і MOC є насправді корисним.
Чарівні Макроси
Чи можна визначити ключові слова, які не є оригінальними ключовими словами C++? signals, slots, Q_OBJECT, emit, SIGNAL, SLOT
. Ці ключові слова відомі як розширення Qt для C++. Насправді вони є просто макросами, визначинеми в qobjectdefs.h
#define signals public
#define slots /* ніщо */
Це правда, що сигнали та слоти - це просто функції і компілятор обробить їх як всі інші функції. Але макроси все таки відіграють свою роль - з ними працює MOC
.
Сигнали були визначені як protected
в Qt4 та старіших версіях. Але в Qt5 їх визначили як public
щоб дозволити реалізувати новий синтаксис сигнально-слотових з’єднань.
#define Q_OBJECT \
public: \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
QT_TR_FUNCTIONS /* помічник для перекладу */ \
private: \
Q_DECL_HIDDEN static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **);
Q_OBJECT
визначає набір функцій і статичний QMetaObject
. Реалізація функцій знаходиться в файлі який згенерував MOC.
#define emit /* ніщо */
emit
це порожній макрос, який не враховується при обробці MOC, є необов’язковим і служить лише як позначка для розробника.
Q_CORE_EXPORT const char *qFlagLocation(const char *method);
#ifndef QT_NO_DEBUG
# define QLOCATION "\0" __FILE__ ":" QTOSTRING(__LINE__)
# define SLOT(a) qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a) qFlagLocation("2"#a QLOCATION)
#else
# define SLOT(a) "1"#a
# define SIGNAL(a) "2"#a
#endif
Ці макроси використовуються препроцесором щоб просто перетворити параметри в стрічку і додати на початку ідентифікатор.
В режимі відлагодження(debug mode) ми дістанемо стрічку з шляхом де розміщено файл для попередження якщо сигнально-слотове з’єднання не працює. Це було додано в Qt 4.5 для сумісності. Щоб дізнатись яка стрічка має інформаційний рядок, ми використовуємо qFlagLocation
який зареєструє адресу стрічки в таблиці з двома входженнями.