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 який зареєструє адресу стрічки в таблиці з двома входженнями.