Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Как работают сигналы и слоты в Qt (часть 2)

Anna | 24.06.2014 | нет комментариев
От переводчика: это вторая часть перевода статьи 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)

1-й способ является способом, особенно близким к ветхому синтаксису: вы соединяете сигнал отправителя со слотом получателя. Другие два перегружают это соединение, подключая к сигналу статическую функцию и функтор без получателя. Все способы дюже схожи и мы в этом посте будем исследовать только 1-й.

Указатель на функции-члены

Раньше чем продолжить моё трактование, я хотел бы немножко побеседовать об указателях на функции-члены. Вот дюже примитивный код, тот, что объявляет указатель на функцию и вызывает её:

// объявление 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), следственно не будет ничего стоить в плане продуктивности во время исполнения.

 

Источник: programmingmaster.ru

 

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB