From the translator: this is the second part of the translation of the Olivier Goffart article on the internal architecture of signals and slots in Qt 5, the translation of the first part here .
New syntax in Qt5
The new syntax is as follows:
QObject::connect(&a, &Counter::valueChanged, &b, &Counter::setValue);
I have already described the advantages of the new syntax in
this post. In short, the new syntax allows you to check 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 the use of lambda expressions.
')
New overloaded methods
Only a few necessary changes were made for this to work. The basic idea is the new QObject :: connect overloads, which take pointers to functions as arguments, instead of char *. These three new methods (pseudocode) are:
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 one closest to the old syntax: you connect the sender's signal to the receiver's 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 one.
Member function pointer
Before continuing my explanation, I would like to talk a little about member function pointers. Here is a very simple code that declares a pointer to a function and calls it:
Pointers to members and pointers to member functions are a common part of a C ++ subset, which is not very often used and therefore less well known. The good news is that you do not need to know about it in order to use Qt and this new syntax. All you need to remember is that you need to position & in front of the signal name in your connection. You do not need to cope 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 a different sizeof. If the function is slightly different in the signature, you will not be able to convert from one to another. For example, even the conversion
void (MyClass :: *) (int) const to
void (MyClass :: *) (int) is not allowed (you can do this with reinterpret_cast, but, in accordance with the standard, there will be undefined behavior (undefined behavior) ), if you try to call a function).
Pointers to member functions are not just ordinary function pointers. A normal function pointer is simply a pointer with an address where the function code is located. But the pointer to a member function needs to store more information: the member function can be virtual and also with 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 to manipulate them.
Property type classes (type traits): QtPrivate :: FunctionPointer
Let me introduce you to a class of properties of type QtPrivate :: FunctionPointer. A property class is basically an auxiliary class that returns some metadata about a given type. Another example of a property class in Qt is
QTypeInfo . What we need to know as part of the implementation of the new syntax is information about a pointer to a function.
The template <typename T> 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, to a member function of which a pointer points
- Arguments - presents a list of arguments, typedef of a metaprogramming list
- call (T & function, QObject * receiver, void ** args) is 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 (the variadic template). In other words, we need to specialize our function for a class of properties for each number of arguments. We have four types of specialization: a regular function pointer, 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 FunctionPointer implementation is located at
qobjectdefs_impl.h .
QObject :: connect
The implementation depends on a large number of template code. I will not explain all this. Here is the code for the first new overload from
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;
You noticed in the function signature that sender and receiver are not just QObject * as the documentation indicates. In fact, these are pointers to the FunctionPointer :: Object typename. To create an overload that is enabled only for pointers to member functions,
SFINAE is used, because an 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 errors when compiling when the user made a mistake. If the user has done something wrong, it will be important for him to see the error here, and not in the noodle of the template code in the _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 trigger it. It also knows the type of signal arguments and can do the appropriate type conversion. We use List_Left only by passing the same number of arguments as in the slot, which allows you 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 one, with the difference that instead of storing the method index in the QObjectPrivate :: Connection structure, we store a pointer to QSlotObjectBase.
The reason why 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 link 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 :-).
MOC will generate qt_static_metacall code that 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<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; } } } }
Now, having a signal index, we can work with syntax similar to the previous one.
QSlotObjectBase
QSlotObjectBase is an object passed to connectImpl, which 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 reimplemented through template classes that implement the call and comparison of function pointers. This is implemented by one of the QSlotObject, QStaticSlotObject, or QFunctorSlotObject template classes.
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 don’t need, such as
RTTI . This would lead to a lot of unnecessary data and the proliferation of binary files. To avoid this, QSlotObjectBase was 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 normal pointer to a function that performs three operations that were previously previous virtual functions. Repeated implementations are set to work in the constructor.
Please do not need to go back to your code and change all the virtual functions to this way, because they read that it is good. This is done only in this case, because almost every call to connect will generate a new different type (starting with a QSlotObject that has template parameters that depend on the signal signature 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 at which you made 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 signals from protected to open. Unfortunately, this means that everyone can emit signals. We have not found a way to fix this. We tried the trick with the
emit keyword . We tried to return a special value. But nothing worked. I believe that the advantages of the new syntax will overcome the problems when the 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 signal parameter. Since it is closed, only the object has the right to create it to call the signal.
The MOC will ignore the last QPrivateSignal argument when generating signature information. See
qabstractitemmodel.h for an example.
More template code
The rest of the code is in
qobjectdefs_impl.h and
qobject_impl.h . This is basically a boring template code. I will no longer go deep into the details in this post, but I will go over several points that are worth mentioning.
Metaprogramming list
As mentioned earlier, FunctionPointer :: Arguments is a list of arguments. The code should work with this list: iterate elementwise, get only a part of it, or select this element. That is why QtPrivate :: List can appear as a list of types. Some auxiliary classes for it are QtPrivate :: List_Select and QtPrivate :: List_Left, which return the Nth item in the list and the part of the list containing the first N items.
The List implementation is different for compilers that support templates with a variable number of parameters and that do not support them. With templates with a variable number of parameters:
template<typename... T> struct List;
The argument list simply hides the template parameters. For example, the type of the list containing the arguments (int, Qstring, QObject *) would be:
List<int, QString, QObject *>
Without templates with a variable number of parameters, it will look like a LISP style:
template<typename Head, typename Tail > 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<int, List<QString, List<QObject *, void>>>
Trick ApplyReturnValue
In the function FunctionPointer :: call, args [0] is designed 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 into 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. Should I duplicate a huge amount of code: once for the return value of void and the other 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 this would be good. It becomes interesting when you call it with something other than void:
functionThatReturnsInt(), somethingElse();
Here, the comma will be the callee operator that you can even overload. This is what we do in
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 is just a wrapper over void *. Now, this can be used in the desired helper entity. Here is an example of a case for a functor with no arguments:
static void call(Function &f, void *, void **arg) { f(), ApplyReturnValue<SignalReturnType>(arg[0]); }
This code is inline, so there will be no cost in terms of performance during execution.