How signals and slots work in Qt (part 2)

Original author: Olivier Goffart
  • Transfer


From a translator: this is the second part of the translation of an article by Olivier Goffart on the internal architecture of signals and slots in Qt 5, the translation of the first part is here .

New syntax in Qt5

The new syntax looks like this:

QObject::connect(&a, &Counter::valueChanged, &b, &Counter::setValue);

I have already described the benefits of the new syntax in this post. In short, the new syntax allows checking signals and slots at compile time. It is also possible to automatically convert arguments if they are not of exactly the same type. And, as a bonus, this syntax allows you to use lamda expressions.

New overloaded methods

Only a few necessary changes were made for this to work. The main idea is new QObject :: connect overloads, which take function pointers as arguments instead of char *. These are three new methods (pseudo-code):

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)

The first method is the method closest to the old syntax: you connect the sender signal to the receiver slot. The other two overload this connection by connecting a static function and a functor without a receiver to the signal. All methods are very similar and in this post we will analyze only the first.

Member Function Pointer

Before continuing my explanation, I would like to talk a bit about pointers to member functions. Here is a very simple code that declares a pointer to a function and calls it:

// объявление 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);

Member pointers and member function pointers are a common part of a C ++ subset that is not very commonly used and therefore less known. The good news is that you don’t need to know about this in order to use Qt and this new syntax. All you need to remember is what you need to place & in front of the signal name in your connection. You do not need to deal with the magic operators :: *,. * Or -> *. These magic operators allow you to declare a pointer to a member function and access it. The type of such pointers includes the return type, the class to which the function belongs, the types of all arguments, and the const specifier for the function.

You cannot convert pointers to member functions to anything else, in particular to void, because they have different sizeof. If the function is slightly different in signature, you will not be able to convert from one to another. For example, even the conversion of void (MyClass :: *) (int) const to void (MyClass :: *) (int) is not allowed (you can do this with reinterpret_cast, but, according to the standard, there will be undefined behavior (undefined behavior) ) if you try to call the function).

Member function pointers are not just ordinary function pointers. A regular function pointer is simply a pointer to the address where the function code is located. But the pointer to the member function needs to store more information: the member function can be virtual and also with an offset, if it is hidden, in the case of multiple inheritance. The sizeof of a pointer to a member function may even vary , depending on the class. That is why we need to have a special case for manipulating them.

Type traits classes: QtPrivate :: FunctionPointer

Let me introduce you to a class of properties of type QtPrivate :: FunctionPointer. The property class is basically a helper class that returns some metadata about this type. Another example of a property class in Qt is QTypeInfo . What we need to know in the framework of the implementation of the new syntax is information about the pointer to the function. template struct FunctionPointer will give us information about T through its members:
  • ArgumentCount - A number representing the number of function arguments
  • Object - exists, only for pointers to member functions, it is a typedef of a class, a pointer to a member function of which
  • Arguments - represents a list of arguments, a typedef of a metaprogramming list
  • call (T & function, QObject * receiver, void ** args) - a static function that calls a function with the parameters passed

Qt still supports the C ++ 98 compiler, which means that we, unfortunately, cannot require support for templates with a variable number of arguments (variadic template). In other words, we must specialize our function for a class of properties for each number of arguments. We have four types of specialization: a regular pointer to a function, a pointer to a member function, a pointer to a constant member function, and functors. For each type, we need specialization for each number of arguments. We have support for up to six arguments. We also have a specialization that uses templates with a variable number of arguments, for an arbitrary number of arguments, if the compiler supports templates with a variable number of arguments. The implementation of FunctionPointer is located in qobjectdefs_impl.h .

QObject :: connect

Implementation depends on a lot of boilerplate code. I will not explain all this. Here is the code for the first new overload from qobject.h :

template 
static inline QMetaObject::Connection connect(
    const typename QtPrivate::FunctionPointer::Object *sender, Func1 signal,
    const typename QtPrivate::FunctionPointer::Object *receiver, Func2 slot,
    Qt::ConnectionType type = Qt::AutoConnection)
{
  typedef QtPrivate::FunctionPointer SignalType;
  typedef QtPrivate::FunctionPointer 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::value),
                    ""Signal and slot arguments are not compatible."");
  Q_STATIC_ASSERT_X((QtPrivate::AreArgumentsCompatible::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::Value,
        typename SignalType::ReturnType>(slot);
  return connectImpl(sender, reinterpret_cast(&signal),
                     receiver, reinterpret_cast(&slot), slotObj,
                     type, types, &SignalType::Object::staticMetaObject);
}


You noticed in the function signature that sender and receiver are not just QObject * as the documentation indicates. These are actually pointers to typename FunctionPointer :: Object. To create an overload that is included only for pointers to member functions, SFINAE is used because Object exists in a FunctionPointer only if the type is a pointer to a member function.

Then we start with a bunch of Q_STATIC_ASSERT. They should generate meaningful compilation errors when the user makes a mistake. If the user has done something wrong, it is important that he sees the error here, and not in the template code noodles in _impl.h files. We want to hide the internal implementation so that the user does not worry about it. This means that if you ever see an incomprehensible error in the implementation details, it should be considered as an error that needs to be reported.

Next, we create an instance of QSlotObject, which will then be passed to connectImpl (). QSlotObject is a wrapper over the slot that will help call it. She also knows the type of signal arguments and can do a suitable type conversion. We use List_Left only passing the same number of arguments as in the slot, which allows us to connect a signal to a slot that has fewer arguments than the signal.

QObject :: connectImpl is a private internal function that will perform the connection. It has a syntax similar to the original, with the difference that instead of storing the index of the method in the QObjectPrivate :: Connection structure, we store a pointer to QSlotObjectBase.

The reason we pass & slot as void ** is to be able to compare it if the type is Qt :: UniqueConnection. We also pass & signal as void **. This is a pointer to a pointer to a member function.

Signal Index

We need to make a connection between the signal pointer and the signal index. We use MOC for this. Yes, this means that this new syntax still uses MOC and that there are no plans to get rid of it :-). The MOC will generate code in qt_static_metacall, which compares the parameter and returns the correct index. connectImpl will call the qt_static_metacall function with a pointer to a function pointer.

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(_a[0]);
        void **func = reinterpret_cast(_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;
            }
        }
    }
}

Now that we have the signal index, we can work with syntax similar to the previous one.

QSlotObjectBase

QSlotObjectBase is an object passed to connectImpl that reflects the slot. Before showing the current code, here is QObject :: QSlotObjectBase, which was in 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; }
};

This is basically an interface that is designed to be re-implemented through template classes that implement calling and comparing function pointers. This is implemented by one of the template classes QSlotObject, QStaticSlotObject or QFunctorSlotObject.

Fake virtual table

The problem is that every time you instantiate such an object, you need to create a virtual table that will contain not only a pointer to virtual functions, but also a lot of information that we do not need, such as RTTI . This would lead to a lot of redundant data and the proliferation of binary files. To avoid this, QSlotObjectBase has been changed to not be a polymorphic class. Virtual functions are emulated manually.

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 is a regular function pointer that performs three operations that were previously previous virtual functions. Repeated implementations are set to work in the constructor.

Please do not need to return to your code and change all the virtual functions in this way, because you read that it’s good. This is done only in this case, because almost every call to connect will generate a new different type (starting with QSlotObject, which has template parameters that depend on the signature of the signal and slot).

Protected, open and closed signals

Signals were protected in Qt4 and earlier. It was a design choice that signals should be transmitted by an object when its state changes. They should not be called from outside the object and calling a signal from another object is almost always a bad idea.

However, with the new syntax, you should be able to get the signal address at the point you created the connection. The compiler will only allow you to do this if you have access to the signal. Writing & Counter :: valueChanged will generate a compilation error if the signal was not open.

In Qt5, we had to change the signals from protected to open. Unfortunately, this means that everyone can emit signals. We did not find a way to fix this. We tried the trick with the emit keyword. We tried to return special meaning. But nothing worked. I believe that the benefits of the new syntax will overcome problems when signals are now open.

Sometimes it is even advisable to have the signal closed. This is the case, for example, in QAbstractItemModel, where otherwise, developers usually emit a signal in a derived class that is not what the API wants. They used a preprocessor trick that made the signals closed, but broke the new connection syntax.

A new hack was introduced. QPrivateSignal is an empty structure declared closed in the Q_OBJECT macro. It can be used as the last parameter of the signal. Since it is closed, only the object has the right to create it to call the signal. Mocwill ignore the last argument of QPrivateSignal when generating signature information. See qabstractitemmodel.h for an example.

More boilerplate code

The rest of the code is in qobjectdefs_impl.h and qobject_impl.h . This is basically boring boilerplate code. I will no longer go deep into the details in this post, but I will go over a few points that are worth mentioning.

Metaprogramming List

As stated earlier, FunctionPointer :: Arguments is a list of arguments. The code should work with this list: iterate element by element, get only a part of it, or select this element. This is why QtPrivate :: List can be represented as a list of types. Some helper classes for it are QtPrivate :: List_Select and QtPrivate :: List_Left, which return the Nth element in the list and the part of the list containing the first N elements.

The implementation of List is different for compilers that support templates with a variable number of parameters and which do not support them. With templates with a variable number of parameters:

template struct List;

The argument list simply hides the template parameters. For example, the type of the list containing the arguments (int, Qstring, QObject *) will be like this:

List

Without templates with a variable number of parameters, this will look in LISP style:

template struct List;

Where Tail can be any other List or void, for the end of the list. The previous example in this case looks like this:

List>>

ApplyReturnValue Trick

In the FunctionPointer :: call function, args [0] is used to get the return value of the slot. If the signal returns a value, it will be a pointer to an object with the type of the return value of the signal, otherwise 0. If the slot returns a value, we must copy it to arg [0]. If it is void, we do nothing.

The problem is that it is syntactically incorrect to use the return value of a function that returns void. Do I have to duplicate a huge amount of code: once for a return value of void and another for a value other than void? No, thanks to the comma operator.

In C ++, you can do this:

functionThatReturnsVoid(), somethingElse();

You can replace the comma with a semicolon and all of this would be nice. It gets interesting when you call it with something other than void:

functionThatReturnsInt(), somethingElse();

Here, the comma will be the called statement, which you can even overload. This is what we do in qobjectdefs_impl.h :

template 
struct ApplyReturnValue {
    void *data;
    ApplyReturnValue(void *data_) : data(data_) {}
};
template
void operator,(const T &value, const ApplyReturnValue &container) {
    if (container.data)
        *reinterpret_cast(container.data) = value;
}
template
void operator,(T, const ApplyReturnValue &) {}

ApplyReturnValue is just a wrapper over void *. Now, this can be used in the desired helper entity. Here is an example case for a functor with no arguments:

static void call(Function &f, void *, void **arg) {
    f(), ApplyReturnValue(arg[0]);
}

This code is inline, so it won't cost anything in terms of performance at runtime.

Also popular now: