0

In the Qt world we can put heavy duty tasks on separated classes and make the function calls using signal/slot mechanism which then, if designed appropriately and implemented accordingly will led to a call on a separated thread.

However with all these, seems to me, one can still go and (inappropriately) make the function calls directly. That will led to calls on the same thread.

So I'm wondering if there are ideas on how to write the interfaces to enforce the calls using signal/slot mechanism? Or, all we can do is hope for the best.

KcFnMi
  • 5,516
  • 10
  • 62
  • 136

2 Answers2

1

You can create a thin wrapper object that proxies direct calls into emitting signals that trigger calling slots of an actual worker in a separate thread. Here's a quick and dirty example:

class Task : public QObject {
    Q_OBJECT
public:
    void doStuff();
};

class TaskRunner {
    Task* m_task;
    QThread* m_thread;
public:
    TaskRunner() {
        m_thread = new QThread;
        m_task = new Task;
        connect(this, &TaskRunner::doStuffRequest, m_task, &Task::doStuff);
        m_task->moveToThread(m_thread);
        m_thread->start();
    }

    void doStuff() {
        emit doStuffRequest();
    }

signals:
    void doStuffRequest();
}

You can make a constructor of Task object private and make TaskRunner friend in Task, so users can't instantiate Task object directly.

For a real-world example I would probably make a runner QObject-inherited as well. Also don't forget to stop a thread properly and do a memory cleanup in a destructor.

Bearded Beaver
  • 646
  • 4
  • 21
  • Sounds good. This way the thread is embedded on the non ui side, if I understand it correctly. And the user (the ui side) of it don't need to take care of moving things to another thread. Just make the calls, with connect or direct, and it's going to be smooth. – KcFnMi Jun 12 '22 at 11:48
0

Once you make a slot (a method) public, then you cannot prevent it being called directly. So the only solution is to make the slot private. But then you can only establish the connection from within the class. And therefore you need to pass the instance of the emitting class, e.g. in constructor.

class SomeReceiver: public QObject
{
    Q_OBJECT

public:
    explicit SomeReceiver(SomeEmitter *emitter, QObject *parent = nullptr) : QObject(parent)
    {
        connect(emitter, &SomeEmitter::someSignal, this, &SomeReceiver::someSlot);
    }

private:
    void someSlot() // you cannot call it directly from outside, it is private
    {
        // some stuff here ...
    }
}

(Alternatively, instead of establishing the connection in a constructor, you can make a setter function which would take a pointer to emitter instance and establish the connection. But then there is a danger of this setter being called twice etc. So I would prefer using the constructor...)

You can use it like this:

auto emitter = new SomeEmitter();
auto receiver = new SomeReceiver(emitter);

The code above ensures calling via signals and slots.

And if you want the code to be executed in a secondary thread, then just move the receiver to that thread.

receiver->moveToThread(&someThread);

... and then every time you emit emitter's signal, the conected slot will always be executed in the thread. If you do not move it to secondary thread, it will be executed in the main/GUI thread.

To enforce you move this object to a thread, then you can also move it in the constructor (either by passing also a thread pointer to constructor or by creating a thread in it - but then you need to take care of all the thread destructuring). I would strongly not recommend this, this is extremely ugly design.

PS: If you want to really enforce that some code is run in a thread, then you should inherint from QThread and override its run() method. This is ALWAYS executed in the thread.

  • People don't seem to like the private slots approach, https://stackoverflow.com/questions/72412840/should-we-prefer-qts-private-slots-over-public-slots/72413183?noredirect=1#comment127931205_72413183. And considering emitter is a QWidget and receiver is a QObject, I won't like to pass a QWidget pointer into a QObject class (the opposite sounds better). – KcFnMi Jun 11 '22 at 09:02
  • I strongly disagree with that answer you linked. There is nothing wrong with private slots. Slot is a method like any other, if you want to make it private and not expose to the outer world as a public API (e.g. because it is an implementation detail and you do not want to be responsible for maintaining it as a public API), then just do it and make it private, it is perfectly alright. But if you have a private slot then you need to make the connection inside the class instance, as I have shown in the example above. Emitter being `QWidget` and receiver `QObject`? Why do you think it is bad? – HiFile.app - best file manager Jun 11 '22 at 09:10
  • Btw. there is nothing magical about signals and slots. It is only a very handy and easy-to-use implementation of the observer pattern. And it works well when used with threading. But anyway, apart from this fancyness they are still just normal methods. So I see no reason why we should treat signals and slots like some holy cow and impose some artificial restrictions such as "no private slots". This is a just a mistaken approach. – HiFile.app - best file manager Jun 11 '22 at 09:16
  • "Emitter being QWidget and receiver QObject? Why do you think it is bad?" My problem is with passing a QWidget pointer into a QObject. In my mind it should be the contrary. – KcFnMi Jun 11 '22 at 09:37
  • @KcFnMi Why not pass QWidget pointer to QObject? I see nothing bad about this. It is just two objects where one depends on the other and they have different superclass. Btw. where in my answer you see that I am talking about widgets? Of course, you cannot move a widget to a thread, that would not work! But that is a completely different story which was not discussed in this thread. – HiFile.app - best file manager Jun 11 '22 at 10:02
  • And yet regarding passing `QWidget` pointer to `QObject`: This would be a little bit cumbersome if you kept the pointer to that widget in the object. It would be bad to keep it as a raw pointer because widget could get deleted once its parent gets deleted. It would be even worse to keep it as `shared_ptr` or `unique_ptr`. But it is OK to keep it as `QPointer` which gets automaticlly nullified if the widget gets deleted. But this is another story, I am not keeping the pointer to the widget in any way. And the connection gets automatically disconnected once the widget gets deleted. All is fine. – HiFile.app - best file manager Jun 11 '22 at 10:07