Введение
Каждый класс, использующий сигналы, слоты или систему свойств Qt, должен содержать макрос Q_OBJECT в своём объявлении — но что именно этот макрос делает, и почему без него код, использующий сигналы/слоты, просто не компилируется или не работает? Ответ — в метаобъектной системе Qt (Meta-Object System) и инструменте moc (Meta-Object Compiler), который генерирует дополнительный C++ код во время сборки. В статье разберём, что происходит «под капотом» при использовании Q_OBJECT.
Концепция
Стандарт C++ не предоставляет встроенной рефлексии — невозможно «на лету» узнать список методов класса, их сигнатуры или вызвать метод по имени без специальной поддержки. Qt решает эту проблему собственным препроцессором — moc, который анализирует заголовочные файлы, находящие макрос Q_OBJECT, и генерирует дополнительный исходный файл (moc_classname.cpp), содержащий реализацию QMetaObject для класса — таблицу сигналов, слотов, свойств и Q_INVOKABLE-методов с информацией об их именах, сигнатурах и адресах. Именно эта сгенерированная информация позволяет connect() находить нужный слот по указателю на функцию (в новом синтаксисе) или по строковому имени (в старом, SIGNAL/SLOT синтаксисе), а системе свойств — обращаться к Q_PROPERTY по имени из QML или QObject::setProperty().
Пример кода: Q_OBJECT и метаобъектная система Qt
// widget.h — класс с Q_OBJECT, сигналами и свойством
#pragma once
#include <QObject>
class Widget : public QObject
{
Q_OBJECT
Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged)
public:
explicit Widget(QObject *parent = nullptr) : QObject(parent) {}
int value() const { return m_value; }
void setValue(int value)
{
if (m_value == value) return;
m_value = value;
emit valueChanged(value);
}
signals:
void valueChanged(int value);
public slots:
void reset() { setValue(0); }
private:
int m_value = 0;
};
// Демонстрация возможностей, предоставляемых метаобъектной системой
#include "widget.h"
#include <QMetaObject>
#include <QMetaMethod>
#include <QMetaProperty>
#include <QDebug>
void inspectMetaObject(Widget *widget)
{
const QMetaObject *meta = widget->metaObject();
qDebug() << "Имя класса:" << meta->className();
qDebug() << "Методы:";
for (int i = 0; i < meta->methodCount(); ++i) {
QMetaMethod method = meta->method(i);
qDebug() << " " << method.methodSignature()
<< "тип:" << (method.methodType() == QMetaMethod::Signal ? "сигнал" : "слот/метод");
}
qDebug() << "Свойства:";
for (int i = 0; i < meta->propertyCount(); ++i) {
QMetaProperty prop = meta->property(i);
qDebug() << " " << prop.name() << "=" << prop.read(widget);
}
// Вызов метода по имени без статической ссылки на него — то самое,
// что невозможно в "чистом" C++ без рефлексии
QMetaObject::invokeMethod(widget, "reset");
// Динамическая установка свойства по строковому имени
widget->setProperty("value", 42);
qDebug() << "Значение после setProperty:" << widget->property("value").toInt();
}
Пояснения к коду
widget->metaObject() возвращает указатель на QMetaObject, сгенерированный moc для класса Widget — через него можно перечислить все методы (включая сигналы, слоты и обычные Q_INVOKABLE-методы) и все объявленные через Q_PROPERTY свойства, причём эта информация доступна в рантайме, без необходимости знать конкретные имена на этапе компиляции вызывающего кода.
QMetaObject::invokeMethod(widget, "reset") вызывает метод reset() по строковому имени — это именно тот механизм, на котором строится, например, динамическое связывание QML-кода с C++ объектами, или старый синтаксис connect(sender, SIGNAL(...), receiver, SLOT(...)). Аналогично, setProperty()/property() позволяют читать и писать значения свойств по имени без статической типизации на этапе компиляции — то, что используется системой привязок QML «под капотом» для связывания Q_PROPERTY с QML-выражениями.
Подводные камни
- Забытый запуск moc для заголовочного файла с
Q_OBJECTприводит к ошибкам линковки вида «undefined reference to vtable» — это случается, когда система сборки не настроена автоматически запускать moc для файла (например, при добавленииQ_OBJECTв файл, который система сборки ранее не considерировала нуждающимся в moc), и решается явным указанием файла вHEADERSCMake/qmake или включениемAUTOMOC. Q_OBJECTв классе без наследования отQObject(прямого или через цепочку наследования) не компилируется — макрос предполагает наличие виртуальных методовQObject, переопределяемых в сгенерированном moc-коде, и не может быть применён к произвольному классу самостоятельно.- Множественное наследование от двух классов, оба наследующих
QObject, не поддерживается — Qt допускает только одиночное наследование отQObject(с возможностью дополнительного наследования от классов безQObject, через виртуальное наследование интерфейсов на основе чистых абстрактных классов, рассмотренное в одной из следующих статей). - Накладные расходы метаобъектной системы для очень «горячих» путей кода. Вызов через
QMetaObject::invokeMethod()илиsetProperty()/property()по строковому имени медленнее прямого вызова метода или обращения к полю — для кода, чувствительного к производительности и не требующего динамической диспетчеризации (известный заранее тип и метод), прямой вызов через указатель на функцию предпочтительнее.