cswork / libraryOpen practice workspace

Articles

Qt C++ Interview Questions: Object Ownership, Signals, and Thread Affinity

Practice Qt C++ interview questions with QObject parent ownership, worker thread affinity, direct and queued signals, deferred cleanup, and shutdown races.

Qt C++ interview questions can hide three different questions inside “which thread owns this object?” A QObject parent determines deletion responsibility. Thread affinity determines where its events and queued calls are delivered. An ordinary C++ method call executes in the calling thread. Confusing these contracts creates bugs that a successful connect does not prevent.

This guide traces an original worker, GUI receiver, and shutdown sequence. Official behavior comes from current Qt 6 documentation; the ownership and wait graphs are practice exercises, not reported employer prompts. The connection sketches are source-reviewed, not compiled or executed against Qt. Our local checks verify the stated models only. Use How a C++ Mutex Protects Shared State alongside the examples to distinguish data synchronization from event delivery.

Who Deletes a QObject With a Parent?

Official behavior: Qt's object ownership guide describes parent-child object trees. Deleting a parent deletes its children. Deleting a child removes it from the parent's child list. A QObject parent is therefore a lifetime relationship, not just a way to organize the interface visually.

Consider a heap-allocated settings window with a child timer. Destroying the window should destroy the timer through that tree. Adding a second independent owning smart pointer to the same timer makes deletion responsibility harder to reason about. A raw pointer can be an observer; it need not imply missing ownership when the parent owns the child.

The interesting follow-up is stack construction order. In our original non-GUI example, create the parent before its stack child:

QObject session;
QObject audit(&session);

At scope exit, audit is destroyed first and detaches itself. Then session is destroyed. The object tree and C++ reverse construction order agree.

Now reverse construction and attach the parent afterward:

QObject audit;
QObject session;
audit.setParent(&session); // deliberately unsafe lifetime arrangement

At scope exit, session is destroyed first. Its child list includes a stack object that has not yet reached its own normal scope destruction. This arrangement violates the intended stack lifetime; do not rely on parent cleanup to make it safe. Our destruction-order model demonstrates the conflicting responsibilities, not a captured runtime crash.

Interview reasoning: draw the owning relationship and the construction order before proposing delete. Naming the object “child” does not establish that it was constructed in an order compatible with its storage duration.

Is QPointer an Owner or a Synchronization Mechanism?

Official behavior: QPointer is a guarded pointer to a QObject. It becomes null when that QObject is destroyed. It does not extend the object's lifetime as shared ownership would.

That makes it useful when a controller wants to observe a dialog that the GUI may close independently. The controller can check whether the dialog still exists before using it within the appropriate execution context. It should not expect the guard to keep the dialog alive until some future asynchronous operation finishes.

Design reasoning: a null check and a later call are separate operations. If another thread can delete or mutate the object between them, a guarded pointer is not a lock. Solve the execution and lifetime protocol instead of treating if (pointer) as universal thread safety.

In an interview, explain what prevents destruction during the actual call. For a GUI object handled through the GUI event loop, the surrounding callback and object context may provide that reasoning. For uncontrolled cross-thread access, merely changing a raw pointer to QPointer does not supply it.

Where Do the QThread and Worker Objects Live?

Official behavior: Threads and QObjects distinguishes a QThread QObject from the thread it manages. The QThread object ordinarily lives where it was created. A parentless worker can have its affinity moved to the managed thread. GUI objects such as QWidget must remain in the main thread.

In our exercise, thread G is the GUI thread and thread W performs a finite calculation. The controller creates both the QThread object and a parentless Worker in G, then moves Worker to W before starting work.

Object or operationAffinity or execution contextWhy it matters
Controller QObjectGReceives results through the GUI event loop
QThread QObjectGIts object methods and slots do not automatically run in W
Worker before moveGCreation establishes initial affinity
Worker after successful moveWQueued work is delivered there
worker->calculate() called from GGOrdinary C++ call follows the caller
Queued request delivered to WorkerWReceiver affinity selects event delivery

Official constraint: the QObject reference explains that an object with a parent cannot be moved independently. Children move with their parent, but C++ member pointers do not automatically create QObject child relationships. Inspect parent() and thread() rather than assuming a member timer moved along with Worker.

Preparation advice: check whether moveToThread succeeded using the API available in your installed Qt version. Current Qt 6 documentation exposes a boolean result; older versions have different signatures. Do not write version-agnostic code that assumes every release returns the same value.

Creating the worker with the controller as its parent and then trying to move it is therefore a concrete failure case. Parenting it to the QThread object also does not make that parent live in W.

Trace Direct, Queued, Auto, and Blocking Connections

Official behavior: Qt's connection type reference defines the delivery choices. Direct delivery calls the slot in the emitting thread. Queued delivery schedules it in the receiver's thread. Auto chooses according to the actual emitting thread and receiver affinity at emission time. Blocking queued delivery also makes the emitting thread wait for the slot to return.

Original trace: Worker has affinity W and Controller has affinity G. For the first three rows, the receiving slot belongs to Controller.

Connection and emissionWhere the receiving slot executesWhat the sender does
Direct; emit from WWContinues after the slot returns
Queued; emit from WG, when its event loop delivers the callContinues without waiting for slot completion
Auto; emit from WG through queued deliveryContinues without waiting for slot completion
Auto; emit from G to ControllerG through direct deliveryContinues after the slot returns
Blocking queued; emit from W to GGW waits for the slot to finish

The fourth row is a useful trap. Suppose code in G directly calls a Worker method, and that method emits a signal. Although the Worker object has affinity W, the emission occurred in G. Auto delivery to Controller can therefore be direct. Sender-object affinity is not a substitute for the actual emitting thread.

That result does not make the cross-thread direct Worker call safe. The method may access state that W is simultaneously processing. It demonstrates why explaining only the connection type leaves an important part of the call path unspecified.

Official boundary: the signals and slots guide notes that slots are also normal C++ functions. Calling one directly does not perform a queued dispatch. Function-pointer connection syntax can catch signature mismatches at compile time, but it does not prove correct object lifetime or thread selection.

Build a Worker Contract That Can Actually Finish

Original connection sketch: Controller lives in G, workerThread is its QThread member, and Worker is a parentless QObject moved to W. Assume calculate is a finite slot and acceptResult updates GUI state.

connect(this, &Controller::requestCalculation,
        worker, &Worker::calculate, Qt::QueuedConnection);
connect(worker, &Worker::resultReady,
        this, &Controller::acceptResult, Qt::QueuedConnection);
connect(&workerThread, &QThread::finished,
        worker, &QObject::deleteLater);
workerThread.start();

This sketch states dispatch and cleanup relationships; it omits the class definitions, application startup, and complete shutdown implementation. It is not a standalone program. The result should carry enough identity to reject an obsolete request if the user has already changed inputs.

Official behavior: the QThread reference describes an event-loop-based worker pattern and connecting finished to the worker's deleteLater. It also distinguishes quit from a cooperative interruption request. Asking a thread to quit its event loop does not forcibly cancel arbitrary code that is still running.

If calculate runs for several minutes without returning, a queued Stop slot in W cannot run during that calculation. For long CPU work, design interruption checks inside the work or divide it into bounded pieces. For blocking I/O, define an actual timeout or cancellation mechanism for the I/O operation.

Official behavior: QTimer requires a running event loop and must be started and stopped in its own thread. A worker that blocks its loop prevents timer callbacks and queued requests from being serviced. Moving a timer is not a substitute for leaving the event loop able to run.

Design reasoning: state the maximum time until the worker can observe cancellation. “We have a Stop button” describes an interface; it does not establish a responsive cancellation contract.

Why Can Shutdown Deadlock Even With Correct Affinity?

Original wait graph: the GUI begins shutdown and calls workerThread.wait(). The worker is finishing a calculation but emits a result through a blocking queued connection to the GUI. G waits for W to finish; W waits for G to execute its slot. The GUI cannot deliver that slot while blocked in the wait.

Correctly naming G and W does not remove the cycle. The repair changes dependencies: avoid making worker completion require a blocking GUI callback during shutdown. Reject late results through controller state, use nonblocking result delivery, and arrange for the worker to stop through its cooperative contract.

The same-thread version is simpler. Blocking queued delivery to a receiver in the emitting thread creates a self-wait. The connection reference explicitly warns about that deadlock. Use it only when you can establish an acyclic wait relationship and a receiver capable of servicing the call.

Lifetime reasoning: do not destroy the ordinary QThread member while its managed thread is still running. A controller's shutdown sequence must account for cancellation, worker completion, deferred destruction, and final synchronization. Waiting may be appropriate at an established teardown boundary, but it can freeze the GUI and must not depend on GUI work that the wait prevents.

Also distinguish deleteLater from immediate deletion. It schedules lifecycle work; the documented behavior depends on event-loop and thread-finish conditions. Calling it after the main event loop has stopped does not guarantee that a GUI object's deletion will occur. Trace the relevant shutdown path instead of quoting “Qt handles deletion” without a condition.

Explain Reentrancy Without Claiming Shared-Instance Safety

Official terminology: Qt's reentrancy and thread-safety guide distinguishes independent instances from concurrent access to shared data. A reentrant class is not automatically safe when multiple threads use the same instance.

For our Worker, queued requests processed by one event loop can provide a useful serialization boundary for its internal state. A direct call from G bypasses that boundary. If the design intentionally allows shared access, specify the synchronization and apply it to all relevant accesses, including reads.

A mutex protecting a calculation buffer does not authorize a QWidget update from W. It addresses the buffer's data race, while the GUI object still has its own execution restriction. This is the practical reason to keep ownership, synchronization, and affinity separate in your explanation.

Our local models check parent-before-child destruction order, the connection table's thread choices, and the two-node shutdown wait cycle. They do not prove race freedom or reproduce a Qt event dispatcher. A real implementation should log the current and receiver threads at the disputed boundary and exercise cancellation and teardown under its supported Qt version.

Sources and Further Reading