Kostylik for the signal-slot system in Qt



    Hello everybody. I want to talk about this ... About two weeks ago, for the first time, I needed to work with the GUI under C ++ and, after a little google, I decided to use Qt. Everyone praised him terribly, and indeed at first glance he looked very worthy.

    At second glance, Qt was also a good thing, but due to some restrictions on the implementation of its meto-object compiler, it was necessary to design small crutches. In this essay (I think the word “essay” will be better suited, because it doesn’t pull on the article in volume) I want to talk about how I solved the problems that arose.

    Added: Based on the comments of good people, an edit was made that tells how to do without MOC and without crutches in general.



    How it all began



    It all started with the fact that I needed to make a template controller class as part of the implementation of the MVC architecture in my library. The controller had to interact with heterogeneous integer data, associating QSpinBox with them as a GUI. If we discard all husks, something like this came out:

    Template Controller
    template< typename T_IntegralType >
    class IntegralController {
    private:
    	T_IntegralType *_modelField;
    	QSpinBox *_view;
    	...
    public:
    	QSpinBox *getView() {
    		if (!_view) {
    			_view = new QSpinBox();
    			/* Подписаться на событие от вьюшечки*/
    		}
    		return _view;
    	}
    	. . .
    private:
    	// Метод, который должен срабатывать по событию от вьюшечки
    	void setValue(T_IntegralType inValue) { * modelField = inValue; }
    };
    



    Having sketched the code, I read about event handling in Qt, and realized that to work with events in general and with events from GUI elements in particular, you need to use a system of signals and slots (this system is well disassembled in this article - it seems this is a translation of the official docks) .

    Note about the model-view approach
    There is also such a thing as a delegate system in the framework of the model-view approach in Qt, which allows you to handle the transfer of data between view and model through the implementation of interfaces, without a signal-slot system. For certain reasons, I was not able to properly use the Qt model-view in my library.


    In order for any class to be able to provide slots for the signal-slot of the system, it was necessary that this class inherit from the QObject class and include the Q_OBJECT macro . What for? Then I did not take a steam bath in order to figure it out. It is necessary - it means it is necessary. Without further ado, he added the required things to his template class and described the implementation of GUI event processing:

    Event-driven template controller
    template< typename T_IntegralType >
    class IntegralController : public QObject {
    	Q_OBJECT
    private:
    	T_IntegralType *_modelField;
    	QSpinBox *_view;
    	...
    public:
    	QSpinBox *getView() {
    		if (!_view) {
    			_view = new QSpinBox();
    			QObject::connect(_view, SIGNAL(valueChanged(int)),
    					this, SLOT(valueChanged(int));
    		}
    	}
    	. . .
    private slots:
    	void valueChanged(int inValue) { *_modelField = inValue; }
    };
    



    Everything has gathered. Well, I thought, what a fine fellow I am! - and continued to saw the library, forgetting about this template controller for a while. Of course, in vain he hurried and generally rejoiced early, because he was not good at it. As soon as I tried to compile code that uses the specialization of the template controller, linker error messages rained down, like that:

    Link Errors
    unresolved external symbol "public: virtual struct QMetaObject const * __cdecl TClass <int> :: metaObject (void) const" (? metaObject @? $ TClass @ H @@ UEBAPEBUQMetaObject @@ XZ)
    unresolved external symbol "public: virtual void * __cdecl TClass <int> :: qt_metacast (char const *)" (? qt_metacast @? $ TClass @ H @@ UEAAPEAXPEBD @ Z)
    unresolved external symbol "public: virtual int __cdecl TClass <int> :: qt_metacall (enum QMetaObject :: Call, int, void * *)" (? qt_metacall @? $ TClass @ H @@ UEAAHW4Call @ QMetaObject @@ HPEAPEAX @ Z)
    



    It was obvious that I was doing something wrong with the meta-object system. I had to re-read about MOC . The thing turned out to be that when the MOC goes through the source code, it generates additional cpp files in which the implementation of the methods necessary for the meta-object of the system to be created is created. This system can work with templates very crookedly, poorly generating this same meta-object code - MOC either simply ignores template classes when generating code, or perceives them as ordinary classes, discarding template arguments, which for obvious reasons caused the above problems.

    More about templates and Qt
    Regarding why you should not use templates together with the signal-slot system, there is even a separate article in the dock. Unfortunately, in my case, it was not about optimality, but about a decent reduction in the amount of code and avoiding a large amount of copy-paste - so for me the points from this article were not suitable.


    Upon further familiarization with the MOC, there were several more restrictions on the use of MOC-controlled classes. The most unpleasant of them is the inability to describe fully managed MOC descendants of QObject in nested classes. And I like to make nested classes, breaking down the areas of responsibility of a large class between the smaller classics living inside it. Yes, I know, labor programmers use namespaces, but at the same time, in my opinion, the context of the program is cluttered, and the semantics of nested classes are different from the semantics of namespaces (in the first case we build a hierarchy of relations between classes, in the second - just group them according to which Something).

    In general, taking into account the fact that I only needed the ability to subscribe to events through the static method from the MOC systemQObject :: connect (...) , I decided to write a small crutch ... A very small, small crutch.

    Note: Kostylik, as it turned out, turned out for a version of Qt younger than the fifth, where the crooked MOC ruled. The Qt 5 version has a cool API that allows signal slots without this MOC. Added this information at the end of the article. Again, morality for me, a lazy author of articles, who is not at all well done: seven times google, once write an article .

    About crutch



    The idea was simple - to make a mini-class that would inherit QObject and would be completely suitable for MOC in the sense of registering the class for correct operation within the signal-slot system. This mini-class would also provide a method for associating a Qt-independent callback with a slot call in this auxiliary class.

    It sounds complicated, the example will be clearer, I hope. In the code for handling events from QSpinBox, it looked like this:

    Kostylik for events from QSpinBox
    class ValueChangedWorkaround : public QObject {
    	Q_OBJECT
    public:
    	// Тип-функтор, реализующий коллбек. В моём случае это
    	// был FastDelegate (ссылка на либу в этом спойлере, ниже)
    	typedef fastdelegate::FastDelegate1 < int > Callback;
    private:
    	Callback _callback;
    public:
    	ValueChangedWorkaround() : _callback() { }
    	void bind(QSpinBox *inSpinBox, const Callback &inCallback) {
    		_callback = inCallback;
    		QObject::connect(inSpinBox, SIGNAL(valueChanged(int)),
    				this, SLOT(valueChanged(int));
    	}
    private slots:
    	void valueChanged(int inValue) { _callback(inValue); }
    };
    


    About FastDelegate in general
    FastDelegate on GitHub


    I used this code in the controller - and it worked:

    Crutch controller
    template< typename T_IntegralType >
    class IntegralController {
    private:
    	typedef IntegralController< IntegralType > OwnType;
    	T_IntegralType *_modelField;
    	QSpinBox *_view;
    	ValueChangedWorkaround _valueChangedWorkaround;
    	...
    public:
    	QSpinBox *getView() {
    		if (!_view) {
    			_view = new QSpinBox();
    			_valueChangedWorkaround.bind(_view,
    				ValueChangedWorkaround::Callback(
    					&OwnType::valueChanged));
    		}
    	}
    	. . .
    private:
    	void valueChanged(int inValue) { *_modelField = inValue; }
    };
    



    It would seem that this could calm down ... But I'm a pathological fan of universal solutions, so I decided to create a macro that allows you to create crutches to handle various events from Qt-objects on an industrial scale.

    Lord of crutches

    Crutch Generator



    It would seem that a new macro can be made on the basis of the old one, simply replacing some identifiers with macro arguments and slightly generalizing the macro itself.

    Macro to generate crutches. Version 1.0
    I omit the slashes here (like this: "\") - enrage terribly!

    define QT_EVENT_WORKAROUND_1_ARG(M_WorkaroundName, M_EventName, M_Arg0Type)
    class M_WorkaroundName : public QObject {
    	Q_OBJECT
    public:
    	typedef fastdelegate::FastDelegate1 < M_Arg0Type > Callback;
    private:
    	Callback _callback;
    public:
    	M_WorkaroundName() : _callback() { }
    	void bind(QObject *inQSignalSource, const Callback &inCallback) {
    		_callback = inCallback;
    		QObject::connect(inQSignalSource, SIGNAL(M_EventName(M_Arg0Type)),
    				this, SLOT(M_EventName(M_Arg0Type));
    	}
    private slots:
    	void M_EventName(M_Arg0Type inValue) { _callback(inValue); }
    };
    



    Having written this macro, I thought with confidence that now I am certainly well done and generally the lord of crutches (like that man above). Very pleased, I ran the code and ... Yes, of course, nothing worked. There were no compilation errors, everything was collected, but the callback was not called, and messages were sent to the log that, allegedly, my TestWorkaround class does not have the necessary slot.

    I had to dig further. It turned out that MOC in Qt is not able to deploy macros. It goes through the code before the preprocessor is executed (that is, not by the code that, for example, can be seen if you build with the -E flag in MinGW, but by the code that has not been processed).
    On the other hand, the MOC must know the method signatures located in the class declaration blocks after the word “slots” - it reads them as strings and then uses these string names when calling QObject :: connect (the SLOT and SIGNAL macros extract these names + some metadata about place of use). Thus, it became clear that the macro user to generate crutches would have to be required to write their own implementation of the slot.

    I tried to minimize the volume and complexity of this code, and the final solution looks like this (already the final code, with godless slashes, yeah):

    #define SIGNAL_WORKAROUND_1_ARG(M_WorkaroundName, M_CallName, M_Arg0Type)\
    class M_WorkaroundName : public QObject {\
    public:\
    	typedef fastdelegate::FastDelegate1< M_Arg0Type > Delegate;\
    \
    private:\
    	Delegate _delegate;\
    \
    public:\
    	void bind(QObject *inQSignalSource, const Delegate &inDelegate) {\
    		_delegate = inDelegate;\
    		QObject::connect(inQSignalSource, SIGNAL(M_CallName(M_Arg0Type)),\
    				this, SLOT(M_CallName(M_Arg0Type)));\
    	}\
    \
    	void CALL(M_Arg0Type inArgument) { _delegate(inArgument); }\
    
    - As you can see, the macro does not fully describe a class, which the user needs to finish with a description of the slot for calling the CALL (...) method. Full instructions for using the crutch generator below ...

    Full instructions:
    1. Generate a crutch class somewhere using a macro. In the example, we will generate a crutch called YourWorkaroundName , which wraps the qtEventName event , which takes one argument of type EventArg1Type ). Code to generate the class:

    SIGNAL_WORKAROUND_1_ARG(YourWorkaroundName, qtEventName, EventArg1Type)
    	Q_OBJECT private slots: void qtEventName(EventArg1Type a0) { CALL(a0); }
    };
    


    2. Use a new type anywhere in the code where you need to process events from any Qt objects that can send an event of a wrapped type (in the example, the qtEventName event sending one argument of type EventArg1Type ). Example code using a crutch:

    class UserClass {
    private:
    	QSomeObject *_qTestObject;
    	YourWorkaroundName _workaround;
    public:
    	UsingClass() : _qTestObject(new QSomeObject()) {
    		_workaround.bind(_qTestObject,
    				YourWorkaroundName::Callback(&UsingClass:: onEvent));
    	}
    	void onEvent(EventArg1Type inArg) { /* Some actions on callback */ }
    }
    


    All is ready. Now you can process messages from Qt objects in any classes, without the restrictions imposed by Qt MOC.

    In conclusion, a few comments:
    1. The proposed macro is suitable for events that take one argument to the input. To process a different number of arguments, you can either copy-paste this macro (old-style style) or use the variadic macro from C ++ 11.
    2. The proposed solution uses the FastDelegate library to work with callbacks. You can easily replace FastDelefate in a macro with your type if you want to use your functors.
    3. In this solution, there is no error handling, assertions, etc. - in my case, such processing is not required. You can add to taste.
    4. I am ready to agree that the proposed solution is a hell of a hell and I will gladly listen to suggestions how else could I deal with the limitations of Qt MOC. I will add your suggestions to the article indicating the authorship of the decision and with gratitude from myself personally. Thanks in advance!

    Conclusion



    I hope the proposed crutch generator will help save some time for someone to write a similar sheet. In the end, I also note that the crutch generator contains a small amount of code, so, as for me, it makes no sense to post it on GitHub.
    However, at the request of the venerable public, I can make a small turnip with a test drive. Speak out in the comments about, if there are more than five who wish, I'll post it.

    Thank you for reading and for reading!

    PS: If you find any errors in the article - write, I will edit.


    (Posted on 03/11/2016 )

    Thanks AlexPublic and VioletGiraffe , poked their nose into the new API signal slot .

    If we talk about the crutch generator, it will look like this:

    Crutch Generator
    #define SIGNAL_WORKAROUND_1ARG(M_WorkaroundName, M_CallName, M_Arg0Type)\
    class M_WorkaroundName : public QObject {\
    public:\
        typedef fastdelegate::FastDelegate1< M_Arg0Type > Delegate;\
    \
    private:\
        Delegate _delegate;\
    \
    public:\
        template< typename T_ListenerType >\
        void bind(T_ListenerType *inQSignalSource, const Delegate &inDelegate) {\
            _delegate = inDelegate;\
            connect(inQSignalSource,\
                        static_cast< void (T_ListenerType::*)(M_Arg0Type) > &T_ListenerType::M_CallName),\
                        this, &M_WorkaroundName::M_CallName);\
        }\
    \
        void M_CallName(M_Arg0Type a0) { _delegate(a0); }\
    };
    


    Use has become easier. Now you can create a wrapper type anywhere, including a template context. Something like this:

    template
    class UserClass {
    private:
    	SIGNAL_WORKAROUND_1ARG(YourWorkaroundName, qtEventName, EventArg1Type);
    	T_SomeQObject *_qTestObject;
    	YourWorkaroundName _workaround;
    public:
    	UsingClass() : _qTestObject(new T_SomeQObject()) {
    		_workaround.bind(_qTestObject, YourWorkaroundName::Callback(&UsingClass::onEvent));
    	}
    	void onEvent(EventArg1Type inArg) { /* Some actions on callback */ }
    }
    



    Although, it should be noted that this new Qt5 API signal-slot allows you to completely get rid of the MOC and its limitations. Therefore, instead of using a crutch, it is easier to inherit a listener class from QObject (since there is no more binding to MOC, the listener heir can be template or whatever), overlaying QObject with an ifdef if you want to make a library with support for different GUI frameworks, or, if you use C ++ 11 and older, and use a lambda , as suggested by AlexPublic .

    Also popular now: