От переводчика: это вторая часть перевода статьи Olivier Goffart о внутренней архитектуре сигналов и слотов в Qt 5, перевод первой части тут.
Новый синтаксис в Qt5
Новый синтаксис выглядит так:
QObject::connect(&a, &Counter::valueChanged, &b, &Counter::setValue);
Я уже описывал преимущества нового синтаксиса в этом посте. Вкратце, новый синтаксис позволяет проверять сигналы и слоты во время компиляции. Также возможна автоматическая конвертация аргументов, если они не имеют точно такой же тип. И, как бонус, этот синтаксис позволяет использовать лямда-выражения.
Новые перегруженные методы
Было сделано лишь несколько необходимых изменений, чтобы это работало. Основная идея заключается в новых перегрузках QObject::connect, которые в качестве аргументов принимают указатели на функции, вместо char*. Вот эти три новых метода (псевдокод):
QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction slot, Qt::ConnectionType type);
QObject::connect(const QObject *sender, PointerToMemberFunction signal, PointerToFunction method)
QObject::connect(const QObject *sender, PointerToMemberFunction signal, Functor method)
Первый метод является методом, наиболее близким к старому синтаксису: вы соединяете сигнал отправителя со слотом получателя. Другие два перегружают это соединение, подключая к сигналу статическую функцию и функтор без получателя. Все методы очень похожи и мы в этом посте будем анализировать только первый.
Указатель на функции-члены
Прежде чем продолжить моё объяснение, я хотел бы немного поговорить об указателях на функции-члены. Вот очень простой код, который объявляет указатель на функцию и вызывает её:
// объявление myFunctionPtr указателем на функцию-член
// которая возвращает void и имеет один параметр int
void (QPoint::*myFunctionPtr)(int);
myFunctionPtr = &QPoint::setX;
QPoint p;
QPoint *pp = &p;
(p.*myFunctionPtr)(5); // вызов p.setX(5);
(pp->*myFunctionPtr)(5); // вызов pp->setX(5);
Указатели на члены и указатели на функции-члены это обычная часть подмножества C++, которая не очень часто используется и поэтому менее известна. Хорошей новостью является то, что вам не нужно знать про это, чтобы использовать Qt и этот новый синтаксис. Всё, что вам необходимо запомнить, это то, что необходимо расположить & перед именем сигнала в вашем соединении. Вам не нужно справляться с магическими операторами ::*, .* или ->*. Эти магические операторы позволяют объявлять указатель на функцию-член и получать к нему доступ. Тип таких указателей включает возращаемый тип, класс, которому принадлежит функция, типы всех аргументов и спецификатор const для функции.
Вы не можете конвертировать указатели на функции-члены во что-либо еще, в частности, к void, потому что они имеют различный sizeof. Если функция немного отличается в сигнатуре, у вас не получится конвентировать из одного в другое. К примеру, не допускается даже преобразование void (MyClass::*)(int) в void (MyClass::*)(int) (вы можете это сделать с reinterpret_cast, но, в соответствии с стандартом, будет неопределённое поведение (undefined behaviour), если вы попробуете вызвать функцию).
Указатели на функции-члены это не только обычные указатели на функции. Обычный указатель на функцию это просто указатель с адресом, где расположен код функции. Но указателю на функцию-член нужно хранить больше информации: функция-член может быть виртуальной и также со смещением, если она будет скрытой, в случае множественного наследования. sizeof указателя на функцию-член может даже менятся, в зависимости от класса. Вот почему нам необходимо иметь особый случай для манипулирования ними.
Классы свойств типов (type traits): QtPrivate::FunctionPointer
Позвольте мне представить вам класс свойств типа QtPrivate::FunctionPointer. Класс свойств, в основном, это вспомогательный класс, который возвращает некоторые метаданные про данный тип. Другим примером класса свойств в Qt является QTypeInfo. То, что нужно нам знать в рамках реализации нового синтаксиса — это информация про указатель на функцию. template<typename T> struct FunctionPointer даст нам информацию о T через свои члены:
- ArgumentCount — число, представляющее количество аргументов функции
- Object — существует, только для указателей на функции-члены, это typedef класса, на функцию-член которого указывает указатель
- Arguments — представляет список аргументов, typedef списка метапрограммирования
- call(T &function, QObject *receiver, void **args) — статическая функция, которая вызывает функцию с переданными параметрами
Qt по прежнему поддерживает компилятор C++98, что означает, что мы, к сожалению, не можем требовать поддержку шаблонов с переменным числом аргументов (variadic template). Другими словами, мы должны специализировать нашу функцию для класса свойств для каждого числа агрументов. У нас есть четыре типа специализации: обычный указатель на функцию, указатель на функцию-член, указатель на константную функцию-член и функторы. Для каждого типа, нам необходима специализация для каждого числа аргументов. У нас есть поддержка до шести аргументов. У нас также есть специализация, которая использует шаблоны с переменным числом аргументов, для произвольного числа аргументов, если компилятор поддерживает шаблоны с переменным числом аргументов. Реализация FunctionPointer расположена в qobjectdefs_impl.h.
QObject::connect
Реализация зависит от большого количества шаблонного кода. Я не буду объяснять всё это. Вот код первой новой перегрузки из qobject.h:
template <typename Func1, typename Func2>
static inline QMetaObject::Connection connect(
const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,
const typename QtPrivate::FunctionPointer<Func2>::Object *receiver, Func2 slot,
Qt::ConnectionType type = Qt::AutoConnection)
{
typedef QtPrivate::FunctionPointer<Func1> SignalType;
typedef QtPrivate::FunctionPointer<Func2> SlotType;
// ошибка при компиляции, если есть несоответствие аргументов
Q_STATIC_ASSERT_X(int(SignalType::ArgumentCount) >= int(SlotType::ArgumentCount),
""The slot requires more arguments than the signal provides."");
Q_STATIC_ASSERT_X((QtPrivate::CheckCompatibleArguments<typename SignalType::Arguments,
typename SlotType::Arguments>::value),
""Signal and slot arguments are not compatible."");
Q_STATIC_ASSERT_X((QtPrivate::AreArgumentsCompatible<typename SlotType::ReturnType,
typename SignalType::ReturnType>::value),
""Return type of the slot is not compatible with the return type of the signal."");
const int *types;
/* ... пропущена инициализация типов, используемых для QueuedConnection ...*/
QtPrivate::QSlotObjectBase *slotObj = new QtPrivate::QSlotObject<Func2,
typename QtPrivate::List_Left<typename SignalType::Arguments, SlotType::ArgumentCount>::Value,
typename SignalType::ReturnType>(slot);
return connectImpl(sender, reinterpret_cast<void **>(&signal),
receiver, reinterpret_cast<void **>(&slot), slotObj,
type, types, &SignalType::Object::staticMetaObject);
}
Вы заметили в сигнатуре функции, что sender и receiver не просто QObject* как указывает документация. На самом деле, это указатели на typename FunctionPointer::Object. Для создания перегрузки, которая включается только для указателей на функции-члены, используется SFINAE, потому что Object существует в FunctionPointer, только если тип будет указателем на функцию-член.
Затем мы начинаем с кучей Q_STATIC_ASSERT. Они должны генерировать осмысленные ошибки при компиляции, когда пользователь сделал ошибку. Если пользователь сделал что-то не так, важным будет чтобы он видел ошибку тут, а не в лапше шаблонного кода в _impl.h файлах. Мы хотим скрыть внутреннюю реализацию, чтобы пользователь не беспокоился о ней. Это означает, что если вы когда-то видите непонятную ошибку в деталях реализации, она должна быть рассмотрена как ошибка, о которой нужно сообщить.
Далее, мы создаем экземпляр QSlotObject, который затем будет передан в connectImpl(). QSlotObject это обёртка над слотом, которая поможет вызвать его. Она также знает тип аргументов сигнала и может сделать подходящее преобразование типа. Мы используем List_Left только передавая то же количество аргументов, как в слоте, что позволяет соединять сигнал со слотом, у которого количество аргументов меньше, чем у сигнала.
QObject::connectImpl это закрытая внутренняя функция, которая выполнит соединение. Она имеет синтаксис, похожий на оригинальный, с отличием, что вместо хранения индекса метода в структуре QObjectPrivate::Connection, мы храним указатель на QSlotObjectBase.
Причина, почему мы передаём &slot как void** в том, чтобы иметь возможность сравнить его, если тип Qt::UniqueConnection. Мы также передаём &signal как void**. Это указатель на указатель на функцию-член.
Индекс сигнала
Нам необходимо сделать связь между указателем на сигнал и индексом сигнала. Мы используем MOC для этого. Да, это означает, что этот новый синтаксис всё еще использует MOC и что нет планов избавиться от этого :-). MOC будет генерировать код в qt_static_metacall, который сравнивает параметр и возвращает правильный индекс. connectImpl будет вызывать функцию qt_static_metacall с указателем на указатель на функцию.
void Counter::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
/* .... пропущено ....*/
} else if (_c == QMetaObject::IndexOfMethod) {
int *result = reinterpret_cast<int *>(_a[0]);
void **func = reinterpret_cast<void **>(_a[1]);
{
typedef void (Counter::*_t)(int );
if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&Counter::valueChanged)) {
*result = 0;
}
}
{
typedef QString (Counter::*_t)(const QString & );
if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&Counter::someOtherSignal)) {
*result = 1;
}
}
{
typedef void (Counter::*_t)();
if (*reinterpret_cast<_t *>(func) == static_cast<_t>(&Counter::anotherSignal)) {
*result = 2;
}
}
}
}
Теперь, имея индекс сигнала, мы может работать с синтаксисом, похожим на предыдущий.
QSlotObjectBase
QSlotObjectBase это объект, передаваемый в connectImpl, который отражает слот. Прежде чем показывать текущий код, вот QObject::QSlotObjectBase, который был в Qt5 alpha:
struct QSlotObjectBase {
QAtomicInt ref;
QSlotObjectBase() : ref(1) {}
virtual ~QSlotObjectBase();
virtual void call(QObject *receiver, void **a) = 0;
virtual bool compare(void **) { return false; }
};
Это в основном интерфейс, который предназначен для повторной реализации через шаблонные классы, реализующие вызов и сравнение указателей на функции. Это реализовано одним из шаблонных классов QSlotObject, QStaticSlotObject или QFunctorSlotObject.
Фальшивая виртуальная таблица
Проблема в том, что при каждом инстанцировании такого объекта нужно создать виртуальную таблицу, которая будет содержать не только указатель на виртуальные функции но и много информации, нам не нужной, такой как RTTI. Это привело бы к большому количеству лишних данных и разрастанию двоичных файлов. Чтобы этого избежать, QSlotObjectBase был измененён, чтобы не быть полиморфным классом. Виртуальные функции эмулируются вручную.
class QSlotObjectBase {
QAtomicInt m_ref;
typedef void (*ImplFn)(int which, QSlotObjectBase* this_,
QObject *receiver, void **args, bool *ret);
const ImplFn m_impl;
protected:
enum Operation { Destroy, Call, Compare };
public:
explicit QSlotObjectBase(ImplFn fn) : m_ref(1), m_impl(fn) {}
inline int ref() Q_DECL_NOTHROW { return m_ref.ref(); }
inline void destroyIfLastRef() Q_DECL_NOTHROW {
if (!m_ref.deref()) m_impl(Destroy, this, 0, 0, 0);
}
inline bool compare(void **a) { bool ret; m_impl(Compare, this, 0, a, &ret); return ret; }
inline void call(QObject *r, void **a) { m_impl(Call, this, r, a, 0); }
};
m_impl это обычный указатель на функцию, выполняющий три операции, которые ранее были предыдущими виртуальные функции. Повторные реализации устанавливаются для работы в конструкторе.
Пожалуйста, не нужно возращаться к своему коду и менять все виртуальные функции на такой способ, потому прочитали, что это хорошо. Это сделано только в этом случае, потому что почти каждый вызов connect будет генерироваться новый другой тип (начиная с QSlotObject, имеющего шаблонные параметры, которые зависят от сигнатуры сигнала и слота).
Защищённые, открытые и закрытые сигналы
Сигналы были защищены (protected) в Qt4 и ранее. Это был выбор дизайна, что сигналы должны передаваться объектом, когда изменяется его состояние. Они не должны вызыватся извне объекта и вызов сигнала из другого объекта почти всегда плохая идея.
Однако, с новым синтаксисом, вы должны быть в состоянии получить адрес сигнала в точке создания вами соединения. Компилятор будет позволять вам сделать это только если вы будете иметь доступ к сигналу. Написание &Counter::valueChanged будет генерировать ошибку при компиляции, если сигнал не был открытым.
В Qt5 нам пришлось изменить сигналы от защищённых к открытым. К сожалению, это означает, что каждый может испускать сигналы. Мы не нашли способ исправить это. Мы пробовали трюк с ключевым словом emit. Мы пытались возвращать специальное значение. Но ничего не работало. Я верю, что преимущества нового синтаксиса преодолеют проблемы, когда сигналы сейчас открыты.
Иногда это даже желательно иметь сигнал закрытым. Это тот случай, например, в QAbstractItemModel, где в противном случае, разработчики, как правило, испускают сигнал в производном классе, который не является тем, что хочет API. Они использовали трюк с препроцессором, который сделал сигналы закрытыми, но сломал новый синтаксис соединения.
Был введён новый хак. QPrivateSignal это пустая структура, объявленная закрытой в макросе Q_OBJECT. Она может быть использована в качестве последнего параметра сигнала. Так как она является закрытой, только объект имеет право на ее создания для вызова сигнала. MOC проигнорирует последний аргумент QPrivateSignal во время создания информации о сигнатуре. Посмотрите qabstractitemmodel.h для примера.
Больше шаблонного кода
Остаток кода в qobjectdefs_impl.h и qobject_impl.h. Это, в основном, скучный шаблонный код. Я не буду больше вдаватся глубоко в подробности в этом посте, но я пройдусь по нескольким пунктам, которые стоит упомянуть.
Список метапрограммирования
Как было указано ранее, FunctionPointer::Arguments это список аргументов. Код должен работать с этим списком: итерировать поэлементно, получить только часть его или выбрать данный элемент. Вот, почему QtPrivate::List может представлятся списком типов. Некоторыми вспомогательными классами для него есть QtPrivate::List_Select и QtPrivate::List_Left, которые возвращают N-ый элемент в списка и часть списка, содержащую первые N элементов.
Реализация List отличается для компиляторов, которые поддерживают шаблоны с переменным числом параметров и которые их не поддерживают. С шаблонами с переменным числом параметров:
template<typename... T> struct List;
Список аргументов просто скрывает шаблонные параметры. Для примера, тип списка, содержащего аргументы (int, Qstring, QObject*) будет таким:
List<int, QString, QObject *>
Без шаблонов с переменным числом параметров, это будет выглядеть в LISP-стиле:
template<typename Head, typename Tail > struct List;
Где Tail может быть любым другим List или void, для конца списка. Предыдущий пример в этом случае выглядит так:
List<int, List<QString, List<QObject *, void>>>
Уловка ApplyReturnValue
В функции FunctionPointer::call, args[0] предназначен для получения возвращаемого значения слота. Если сигнал возвращет значение, это будет указатель на объект с типом возвращаемого значения сигнала, в противном случае 0. Если слот возвращает значение, мы должны копировать его в arg[0]. Если же это void, мы ничего не делаем.
Проблема в том, что синтаксически некорректно использовать возвращаемое значение функции, которая возвращает void. Должен ли я дублировать огромное количество кода: один раз для возвращаемого значения void и другой – для значения, отличного от void? Нет, спасибо оператору «запятая».
В C++ вы можете делать так:
functionThatReturnsVoid(), somethingElse();
Вы можете заменить запятую на точку с запятой и всё это было бы хорошо. Интересным это становится, когда вы вызываете это с чем-то, отличным от void:
functionThatReturnsInt(), somethingElse();
Теперь, запятая будет на самом деле вызывать то, что вы даже можете перегрузить. Это то, что мы делаем в qobjectdefs_impl.h:
template <typename T>
struct ApplyReturnValue {
void *data;
ApplyReturnValue(void *data_) : data(data_) {}
};
template<typename T, typename U>
void operator,(const T &value, const ApplyReturnValue<U> &container) {
if (container.data)
*reinterpret_cast<U*>(container.data) = value;
}
template<typename T>
void operator,(T, const ApplyReturnValue<void> &) {}
ApplyReturnValue это просто обёртка над void*. Теперь, это может быть использовано в каждой вспомогательной сущности. Вот пример случая, для функтора без аргументов:
static void call(Function &f, void *, void **arg) {
f(), ApplyReturnValue<SignalReturnType>(arg[0]);
}
Этот код встроенный (inline), поэтому не будет ничего стоить в плане производительности во время исполнения.
Автор: blackmaster