Open any Qt codebase and you'll see new everywhere and delete almost
nowhere. That isn't sloppiness: Qt has its own ownership model, older than
std::unique_ptr, built around three ideas:
- The object tree. A
QObjectwith a parent is owned by that parent and deleted by it. - Deferred deletion.
deleteLater()asks the event loop to delete an object once nothing up the call stack can still be using it. - Observed lifetimes.
QPointerand thedestroyedsignal tell you when an object you don't own has gone, and connections end automatically when either end dies (if you give them the right context object).
Interviews for Qt roles probe exactly these: "who deletes this?", "why
deleteLater and not delete?", "what happens to a lambda connected to a
deleted object?", "can I delete this from another thread?". This skill builds
on smart-pointers-ownership (owner vs observer, unique_ptr, weak_ptr) and
maps each Qt tool onto those relationships. Every run snippet here links Qt 6
Core and runs under ASan and UBSan.
1. The object tree: parents own children
Pass a parent to the constructor (or call setParent) and the parent takes
ownership: its destructor deletes every child, recursively. Deleting a child
first is fine too: the child removes itself from its parent's list on the way
out, so nobody deletes it twice.
#include <QCoreApplication>
#include <QObject>
#include <cstdio>
int main() {
int argc = 0;
QCoreApplication app(argc, nullptr);
auto* window = new QObject; // no parent: we own it
window->setObjectName("window");
auto* registry = new QObject(window); // owned by window
registry->setObjectName("registry");
for (const char* name : {"garden", "bicycle"})
(new QObject(registry))->setObjectName(name);
auto* timer = new QObject(window);
timer->setObjectName("timer");
QList<QObject*> all = window->findChildren<QObject*>();
all.prepend(window);
for (QObject* o : all)
QObject::connect(o, &QObject::destroyed,
[](QObject* x) { std::printf("~%s ", qPrintable(x->objectName())); });
std::printf("registry has %lld children\n", (long long)registry->children().size());
delete registry->findChild<QObject*>("garden"); // leaves the tree on its own
std::printf("\nregistry has %lld children\n", (long long)registry->children().size());
delete window; // deletes everything left
std::printf("\n");
}window deletes the whole tree, depth first, in the order the children were added. A child deleted early unlinks itself from its parent.The destruction order is worth knowing precisely, because code runs during it.
QObject::~QObject does, in order:
destroyed is emitted, the derived class's destructor has already run: only the QObject part is left. Connections are cut before the children die.Two practical consequences:
- A
destroyedhandler only gets aQObject*.qobject_castto your derived type fails and calling a derived virtual is wrong: that part is gone. Capture what you need (a name, an id) when you connect. - Step 3 before step 4 is what makes a context object safe: an object's connections, including lambdas it is the context of, are cut before its children are deleted. Section 4 builds on this.
Parents and the stack
Objects with a parent should be heap-allocated (new). A stack object with a
parent is fine only while it dies before its parent: C++ destroys locals in
reverse order of declaration, so the parent must be declared first.
void bad() {
QObject child;
QObject parent;
child.setParent(&parent);
} // ~parent runs first and `delete`s &child, a stack address: crashParents and std::unique_ptr
The tree and unique_ptr are both unique ownership; never let both own the
same object. The usual split: a unique_ptr (or a stack object, or a member)
for the top of a tree, parents for everything below it. To move an object out
of a tree into a unique_ptr, first setParent(nullptr):
std::unique_ptr<QObject> take(QObject* child) {
child->setParent(nullptr); // the parent forgets it
return std::unique_ptr<QObject>(child); // now exactly one owner
}Skip the setParent(nullptr) and it works until the parent dies first — then
the parent deletes the child and the unique_ptr deletes it again. The build
task has exactly this trap.
2. Observers: QPointer and destroyed
QPointer<T> is Qt's weak_ptr for QObjects: a non-owning pointer that
becomes null when the object is destroyed, however that happens. Unlike
weak_ptr there's no lock() — you test it and use it, so it's only safe
when the object lives in your thread (another thread could delete it
between the test and the use).
#include <QCoreApplication>
#include <QObject>
#include <QPointer>
#include <cstdio>
int main() {
int argc = 0;
QCoreApplication app(argc, nullptr);
auto* parent = new QObject;
auto* scene = new QObject(parent);
QObject* raw = scene;
QPointer<QObject> guard = scene;
QObject::connect(scene, &QObject::destroyed, [&](QObject*) {
std::printf("in destroyed: guard is %s\n", guard ? "set" : "null");
});
delete parent;
std::printf("after: guard is %s, raw still holds %s\n",
guard ? "set" : "null", raw ? "an address" : "null");
}raw still holds the old address — dereferencing it would be a
heap-use-after-free. guard is null, already inside the destroyed handler
(step 1 above). Use QPointer for:
- a "current"/"selected" object that someone else owns (the build task's
active()), - a pointer captured into a callback that may run later,
- anything you hand out from a lookup and can't promise stays alive.
Use the destroyed signal when you need to do something at the moment an
object dies, such as dropping it from an index. It fires for every death:
parent destruction, plain delete, deleteLater. An index that only cleans up
the deaths it ordered itself will hold dangling pointers.
3. deleteLater and the event loop
delete is immediate. That's a problem when code up the call stack is still
using the object: you're inside a slot connected to one of its signals, it
has events queued for it, or it lives in another thread. deleteLater()
posts a QEvent::DeferredDelete to the object's thread; the event loop
deletes the object when control gets back to it, after every frame that
might use the object has returned.
#include <QCoreApplication>
#include <QEvent>
#include <QObject>
#include <QPointer>
#include <cstdio>
int main() {
int argc = 0;
QCoreApplication app(argc, nullptr);
QPointer<QObject> p = new QObject;
p->deleteLater();
p->deleteLater(); // harmless: still one deferred delete
std::printf("after deleteLater: %s\n", p ? "alive" : "deleted");
QCoreApplication::processEvents(); // not inside a loop: deferred deletes wait
std::printf("after processEvents: %s\n", p ? "alive" : "deleted");
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete);
std::printf("after sendPostedEvents: %s\n", p ? "alive" : "deleted");
}The rule behind the middle line: a deferred delete runs when control returns
to the event loop that was running when deleteLater was called (or, if none
was, once a loop runs). A bare processEvents() at the top level isn't such a
return, so in tests you flush them explicitly with
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete) — the
build task's tests do. Two more rules from the docs: calling deleteLater
after the main loop has exited means the object is never deleted, and
objects that are destroyed before their deferred delete arrives (a parent
deleted them) simply drop the posted event.
deleteLater turns 'delete it now' into 'delete it once everybody up the stack is done'. Between the two, the registry reports the scene as pending.Qt's own emission code survives a delete sender() inside a slot in most
cases, but your own classes (and Qt's documented rules) promise nothing: code
after an emit routinely touches this. deleteLater is the habit.
4. Connections have lifetimes too
QObject::connect comes in two shapes for lambdas:
connect(sender, &Sender::signal, [this] { use(this); }); // lives as long as sender
connect(sender, &Sender::signal, context, [this] { use(this); }); // ends when either diesThe three-argument form has no idea that the lambda captured this. When
this dies first, the next emission runs the lambda on a dead object. The
four-argument form registers context as the receiver: when the context is
destroyed, step 3 of ~QObject cuts the connection. (The context also
chooses the thread the lambda runs in: if it lives in another thread, the call
is queued — see qt-background-work.) Rule: a lambda that captures this
gets a context whose lifetime covers this — usually this itself, or an
object this owns.
#include <QCoreApplication>
#include <QObject>
#include <cstdio>
int main() {
int argc = 0;
QCoreApplication app(argc, nullptr);
QObject sender;
int with_context = 0, without_context = 0;
auto* receiver = new QObject;
QObject::connect(&sender, &QObject::objectNameChanged, receiver, [&] { ++with_context; });
QObject::connect(&sender, &QObject::objectNameChanged, [&] { ++without_context; });
sender.setObjectName("one");
delete receiver; // disconnects the first connection
sender.setObjectName("two");
std::printf("with context: %d, without: %d\n", with_context, without_context);
}Here the context-less lambda only touches two ints that are still alive. In
real code it captured this of something that's gone — a heap-use-after-free
that only appears when the destruction order is unlucky.
Other tools for ending a connection:
QMetaObject::Connection c = connect(...), laterQObject::disconnect(c). Keep the handle when the relationship ends before either object dies (the build task'srelease()).disconnect(sender, nullptr, receiver, nullptr)cuts everything between two objects.Qt::SingleShotConnection(Qt 6) disconnects after the first call.
Members versus the tree: the order trap
The registry in the build task holds a root QObject and a few containers.
Members are destroyed in reverse declaration order, and the root's destructor
deletes the scenes, whose destroyed handlers touch the containers:
class SceneRegistry {
std::unique_ptr<QObject> root_; // declared first: destroyed last
std::map<QString, Entry> index_; // gone by the time root_ dies
// ...
// connect(scene, &QObject::destroyed, root_.get(), [this, name] { forget(name); });
};With root_.get() as the context, ~QObject of the root disconnects the
handlers before deleting the scenes, so forget never runs on a destroyed
index_. Without it, it does. Both versions compile without a warning.
5. Thread affinity
Every QObject lives in a thread — the one that created it, unless moved
— reported by thread(). Its timers, queued slot calls and deferred deletes
are delivered by that thread's event loop. Rules that follow:
moveToThread(t)must be called from the object's current thread, on an object without a parent; its children move with it.- Children must live in the parent's thread:
new QObject(parent)from another thread is an error Qt warns about. - Delete an object in its own thread. From elsewhere, call
deleteLater(): the deferred delete is delivered by the owning thread's loop. For a worker: connectQThread::finishedtodeleteLater.
#include <QCoreApplication>
#include <QEvent>
#include <QObject>
#include <QPointer>
#include <QThread>
#include <cstdio>
int main() {
int argc = 0;
QCoreApplication app(argc, nullptr);
QThread worker_thread;
auto* worker = new QObject; // no parent: it's going to move
worker->moveToThread(&worker_thread);
QObject::connect(&worker_thread, &QThread::finished, worker, &QObject::deleteLater);
QPointer<QObject> watch = worker;
worker_thread.start();
bool ran_elsewhere = false;
QMetaObject::invokeMethod(worker, [&] { // queued into worker_thread's loop
ran_elsewhere = QThread::currentThread() == &worker_thread;
QMetaObject::invokeMethod(&app, &QCoreApplication::quit, Qt::QueuedConnection);
}, Qt::QueuedConnection);
app.exec();
worker_thread.quit(); // the loop ends: finished → deleteLater
worker_thread.wait(); // the worker was deleted in its thread
std::printf("ran in worker thread: %d, worker deleted: %d\n", ran_elsewhere, watch.isNull());
}finished is emitted from the worker thread as it winds down, so
deleteLater is called there; a finishing QThread then processes its
deferred deletes, so the worker is destroyed in its own thread before wait()
returns — no flush needed in main. Background work, queued connections and clean
shutdown are the subject of qt-background-work.
deleteLater.6. Why Q_OBJECT needs moc
Custom signals, slots invoked by name, Q_PROPERTY, qobject_cast and
QML_ELEMENT all need a class's meta-object: a table of its signals,
methods and properties, plus the function bodies of the signals. C++ can't
produce that by itself, so Qt's moc (meta-object compiler) reads headers
containing Q_OBJECT and generates a moc_*.cpp for each.
class SceneItem : public QObject {
Q_OBJECT // declares staticMetaObject, metaObject(), qt_metacall…
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
public:
explicit SceneItem(QObject* parent = nullptr) : QObject(parent) {}
QString name() const { return name_; }
void setName(const QString& n) {
if (n == name_) return;
name_ = n;
emit nameChanged(n); // `emit` is an empty macro: a plain call
}
signals:
void nameChanged(const QString& name); // body generated by moc
private:
QString name_;
};find_package(Qt6 6.5 REQUIRED COMPONENTS Core)
qt_standard_project_setup() # turns on CMAKE_AUTOMOC (and AUTOUIC)
qt_add_executable(viewer main.cpp sceneitem.h sceneitem.cpp)
target_link_libraries(viewer PRIVATE Qt6::Core)When moc didn't run on a header (AUTOMOC off, header not listed or not next
to its .cpp, Q_OBJECT added without re-running CMake), the link fails with
undefined reference to 'vtable for SceneItem' or to SceneItem::nameChanged
— the generated code is the missing definition. This course's sandbox has
no moc, so graded code here sticks to built-in signals (destroyed,
objectNameChanged, QThread::finished…) connected to lambdas, which need no
generated code. Custom signals belong in the project.
7. Implicit sharing: Qt containers copy lazily
QString, QByteArray, QList, QHash, QMap, QImage and friends are
implicitly shared (copy-on-write): a copy shares the data and bumps an
atomic reference count; the first non-const access on a shared instance
detaches — copies the data for itself.
#include <QList>
#include <QString>
#include <cstdio>
#include <utility>
int main() {
QList<int> a{1, 2, 3};
QList<int> b = a; // O(1): shares the buffer
std::printf("shared after copy: %d\n", a.constData() == b.constData());
for (int x : std::as_const(b)) (void)x; // const access: no detach
std::printf("shared after const loop: %d\n", a.constData() == b.constData());
for (int& x : b) (void)x; // non-const begin(): detaches
std::printf("shared after non-const loop: %d\n", a.constData() == b.constData());
QString s = "splat";
QString t = s;
t[0] = 'S'; // detach, then write
std::printf("%s %s\n", qPrintable(s), qPrintable(t));
}QList b = a;b[0] = 9;How it differs from std::vector and std::string in practice:
| Qt containers | std containers | |
|---|---|---|
| copy | O(1), shares | deep copy |
| non-const access on a copy | may silently copy everything | never copies |
| pointers/references into it | invalidated by a detach, not just by growth | invalidated only by reallocation/erase |
| threads | copies may be used in different threads (atomic refcount); one instance is not thread-safe | the same rule, without the hidden sharing |
Habits: iterate shared Qt containers with std::as_const (or const auto&)
to avoid accidental detaches, prefer constFirst()/at() over first()/[]
for reads, and don't keep a reference into a container that you then copy and
write. Passing const QString& vs QString by value is cheap either way, which
is why Qt APIs return containers by value freely. In Qt 6, QList and
QVector are the same class.
8. Testing lifetime code
Lifetime bugs are deterministic — the same destruction order crashes every time — so they make good unit tests, provided the test drives the event loop explicitly:
- One
QCoreApplicationper process. In a test binary, create it lazily in a function-localstaticand reuse it. - Flush deferred deletes with
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); don't sleep and hope. - Hold a
QPointerto every object whose death you want to assert:CHECK(p.isNull()). - Run under ASan: a use-after-free in a
destroyedhandler or a double delete is reported with the free stack pointing into~QObject. (Qt code can't be checked with TSan: the system Qt isn't instrumented. Test thread-safety on Qt-free code.) - With QtTest,
QSignalSpy spy(obj, &QObject::destroyed)counts emissions; the next skill, qt-models-and-qml, uses it heavily.
In the project
Ungraded practice in projects/scene-viewer/app/ (stage 2); the evidence is
your own build and test output.
- Draw the viewer's object tree and threads:
SceneController, itsSceneListModel(a child — why?), the image provider (owned by the QML engine — why does it hold aQPointer?), the twoLatestWinsworkers and the QML window. Add it todocs/ARCHITECTURE.md. - Find every
QMetaObject::invokeMethod(this, …, Qt::QueuedConnection)inscene_controller.cpp. Explain what happens to a posted call when the controller has been destroyed, and why the destructor shuts the workers down first. app/tests/tst_scene_controller.cppdestroys the controller during active work: run it under ASan (cmake -S . -B build/app-asan -DSPLAT_SANITIZER=address -DSPLAT_BUILD_APP=ON), then break the destructor order on purpose and read what ASan reports.- Add a feature that needs ownership care (e.g. closing a scene: remove the row, drop the scene, cancel its render) with a QtTest test.
Summary
- A parent owns its children and deletes them, depth first; a child deleted
early unlinks itself. Never let a parent and a
unique_ptrboth own an object:setParent(nullptr)to take it out. QPointerobserves a QObject and nulls itself on destruction (same thread only);destroyedfires for every death, after the derived part is gone.deleteLaterdefers deletion to the event loop; flush it in tests withsendPostedEvents(nullptr, QEvent::DeferredDelete).- Connect lambdas that capture
thiswith a context object; keep theQMetaObject::Connectionwhen you need to end the relationship early. - Objects live in threads; delete them in theirs (
deleteLater). Q_OBJECTneeds moc (AUTOMOC); missing moc output shows up as an undefined vtable at link time.- Qt containers are copy-on-write: cheap copies, hidden detaches.