When to use
Use this when you are reviewing Qt 6.x C++ for ownership bugs, signal/slot mistakes, threading hazards, and non-idiomatic Qt usage. Not for: wiring up the build or QML modules (see qt-cmake-project) or writing the API docs (see qt-cpp-docs).
Method
- Check
QObject identity: every class deriving from QObject that declares signals, slots, or Q_PROPERTY must have the Q_OBJECT macro and be reachable by moc. Decision point: if Q_OBJECT is missing, signals/slots and qobject_cast silently break — flag it before anything else.
- Trace ownership: confirm each heap
QObject has a parent or a clear owner. Decision point: parented object → do not delete it manually; unparented → require a smart pointer or explicit lifetime.
- Audit connections: prefer function-pointer
connect(sender, &S::sig, receiver, &R::slot) for compile-time checking over the string-based SIGNAL()/SLOT() form. Check Qt::ConnectionType for cross-thread cases.
- Look for dangling access: raw
QObject* held across time should be QPointer so it nulls on destruction.
- Review value semantics: containers and
QString passed by value where a const T& (or QStringView for read-only string args) would avoid copies.
- Check copy control: classes managing resources or identity should use
Q_DISABLE_COPY or the PIMPL/d-pointer idiom; verify the d-pointer is deleted.
- Inspect implicit sharing (COW): detachments from calling non-const methods, and iterator invalidation when a shared container is modified elsewhere.
- Review threading: an object's thread affinity,
moveToThread, and whether slots touch GUI objects off the main thread.
- Run tooling: clazy (
level0/level1/level2) and clang-tidy against compile_commands.json. Decision point: pre-merge gate → require clazy level1 clean; deep audit → level2.
Example
// FLAG: missing Q_OBJECT -> signals won't fire, moc emits nothing
class Worker : public QObject {
// Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr) : QObject(parent) {}
signals:
void done(int code);
};
// FLAG: string-based connect - no compile-time check, typo compiles
connect(w, SIGNAL(done(int)), this, SLOT(onDone(int)));
// PREFER: function-pointer connect, checked at compile time
connect(w, &Worker::done, this, &Controller::onDone);
// FLAG: raw pointer may dangle if child is destroyed elsewhere
QLabel *label; // -> QPointer<QLabel> label;
// FLAG: pass-by-value copy of a container
void render(QList<QString> items); // -> const QList<QString>&
void setTitle(QString title); // read-only -> QStringView
// Ownership-managing type: forbid copies + use d-pointer
class Session {
Q_DISABLE_COPY(Session)
class Private;
Private *d; // deleted in ~Session
};
Pitfalls
- Missing Q_OBJECT. No meta-object means no signals/slots, no
qobject_cast, no runtime property system — and no moc output.
- Double delete via parent. Deleting a parented
QObject yourself leaves the parent with a dangling child pointer.
- String connects.
SIGNAL()/SLOT() typos fail only at runtime as a silent no-op with a console warning.
- Cross-thread direct call. A
Qt::DirectConnection slot runs in the emitter's thread; touching GUI there is undefined.
- Dangling raw pointer. A cached
QObject* outliving its target crashes; QPointer nulls itself.
- Unnecessary detach. Calling a non-const container method detaches the COW buffer, copying the whole container.
- Copy of identity type. Copying a
QObject-owning wrapper without Q_DISABLE_COPY duplicates ownership and double-frees.
Output format
# Qt C++ Review: <file/target>
## Blocking
- [ownership] <file:line> — <QObject parent / delete issue>
- [signals] <file:line> — string connect / missing Q_OBJECT
## Non-blocking
- [perf] <file:line> — pass const& / QStringView; avoid COW detach
- [threading] <file:line> — connection type / thread affinity
## Tooling
- clazy: level<N> — <clean | findings>
- clang-tidy: <clean | findings>
Reference
Q_OBJECT in the private section is mandatory for signals/slots, Q_PROPERTY, qobject_cast, and runtime type info; the class must be processed by moc.
- Ownership:
QObject(QObject *parent); children are deleted with the parent — never manually delete a parented object still owned.
- Connect (Qt 6):
QMetaObject::Connection connect(sender, &Sender::sig, receiver, &Receiver::slot, Qt::ConnectionType) — function-pointer form is compile-time checked; string SIGNAL()/SLOT() is not.
Qt::ConnectionType: AutoConnection (default), DirectConnection, QueuedConnection, BlockingQueuedConnection, UniqueConnection.
QPointer<T> becomes null when its QObject is destroyed; use for observer pointers.
emit is a no-op macro for readability; signals are invoked like functions.
- Copy control:
Q_DISABLE_COPY(Class) deletes copy ctor/assign; PIMPL uses a Private *d (d-pointer), often via QScopedPointer/Q_DECLARE_PRIVATE.
- Strings: prefer
QStringView for non-owning read-only string parameters; QString is implicitly shared (copy-on-write).
- Implicit sharing (COW): copies are cheap until a mutating (non-const) call triggers a detach; be wary of iterator invalidation across shared copies.
- Threading:
QObject::thread() reports affinity; moveToThread(QThread*) reparents affinity; queued connections marshal calls to the receiver's thread; GUI objects live on the main thread only.
SkipEmptyParts is the Qt 6 QString::split flag (replaces the removed QString::SkipEmptyParts); the string-based connect enum lives under Qt:: too.
- Tooling: clazy check levels
level0 (safe, no false positives), level1 (default), level2 (may have false positives); run clazy and clang-tidy against compile_commands.json.
1---2name: qt-cpp-review3description: Review Qt6 C++ code for correctness, memory ownership, and idiomatic use of QObject, signals/slots, and implicit sharing.4---5## When to use6Use this when you are reviewing Qt 6.x C++ for ownership bugs, signal/slot mistakes, threading hazards, and non-idiomatic Qt usage. **Not for:** wiring up the build or QML modules (see `qt-cmake-project`) or writing the API docs (see `qt-cpp-docs`).78## Method91. Check `QObject` identity: every class deriving from `QObject` that declares signals, slots, or `Q_PROPERTY` must have the `Q_OBJECT` macro and be reachable by `moc`. Decision point: if `Q_OBJECT` is missing, signals/slots and `qobject_cast` silently break — flag it before anything else.102. Trace ownership: confirm each heap `QObject` has a parent or a clear owner. Decision point: parented object → do not `delete` it manually; unparented → require a smart pointer or explicit lifetime.113. Audit connections: prefer function-pointer `connect(sender, &S::sig, receiver, &R::slot)` for compile-time checking over the string-based `SIGNAL()/SLOT()` form. Check `Qt::ConnectionType` for cross-thread cases.124. Look for dangling access: raw `QObject*` held across time should be `QPointer` so it nulls on destruction.135. Review value semantics: containers and `QString` passed by value where a `const T&` (or `QStringView` for read-only string args) would avoid copies.146. Check copy control: classes managing resources or identity should use `Q_DISABLE_COPY` or the PIMPL/d-pointer idiom; verify the d-pointer is deleted.157. Inspect implicit sharing (COW): detachments from calling non-const methods, and iterator invalidation when a shared container is modified elsewhere.168. Review threading: an object's thread affinity, `moveToThread`, and whether slots touch GUI objects off the main thread.179. Run tooling: clazy (`level0`/`level1`/`level2`) and clang-tidy against `compile_commands.json`. Decision point: pre-merge gate → require clazy level1 clean; deep audit → level2.1819## Example20```cpp21// FLAG: missing Q_OBJECT -> signals won't fire, moc emits nothing22class Worker : public QObject {23 // Q_OBJECT24public:25 explicit Worker(QObject *parent = nullptr) : QObject(parent) {}26signals:27 void done(int code);28};2930// FLAG: string-based connect - no compile-time check, typo compiles31connect(w, SIGNAL(done(int)), this, SLOT(onDone(int)));32// PREFER: function-pointer connect, checked at compile time33connect(w, &Worker::done, this, &Controller::onDone);3435// FLAG: raw pointer may dangle if child is destroyed elsewhere36QLabel *label; // -> QPointer<QLabel> label;3738// FLAG: pass-by-value copy of a container39void render(QList<QString> items); // -> const QList<QString>&40void setTitle(QString title); // read-only -> QStringView4142// Ownership-managing type: forbid copies + use d-pointer43class Session {44 Q_DISABLE_COPY(Session)45 class Private;46 Private *d; // deleted in ~Session47};48```4950## Pitfalls51- **Missing Q_OBJECT.** No meta-object means no signals/slots, no `qobject_cast`, no runtime property system — and no moc output.52- **Double delete via parent.** Deleting a parented `QObject` yourself leaves the parent with a dangling child pointer.53- **String connects.** `SIGNAL()/SLOT()` typos fail only at runtime as a silent no-op with a console warning.54- **Cross-thread direct call.** A `Qt::DirectConnection` slot runs in the emitter's thread; touching GUI there is undefined.55- **Dangling raw pointer.** A cached `QObject*` outliving its target crashes; `QPointer` nulls itself.56- **Unnecessary detach.** Calling a non-const container method detaches the COW buffer, copying the whole container.57- **Copy of identity type.** Copying a `QObject`-owning wrapper without `Q_DISABLE_COPY` duplicates ownership and double-frees.5859## Output format60```61# Qt C++ Review: <file/target>62## Blocking63- [ownership] <file:line> — <QObject parent / delete issue>64- [signals] <file:line> — string connect / missing Q_OBJECT65## Non-blocking66- [perf] <file:line> — pass const& / QStringView; avoid COW detach67- [threading] <file:line> — connection type / thread affinity68## Tooling69- clazy: level<N> — <clean | findings>70- clang-tidy: <clean | findings>71```7273## Reference74- `Q_OBJECT` in the private section is mandatory for signals/slots, `Q_PROPERTY`, `qobject_cast`, and runtime type info; the class must be processed by `moc`.75- Ownership: `QObject(QObject *parent)`; children are deleted with the parent — never manually delete a parented object still owned.76- Connect (Qt 6): `QMetaObject::Connection connect(sender, &Sender::sig, receiver, &Receiver::slot, Qt::ConnectionType)` — function-pointer form is compile-time checked; string `SIGNAL()/SLOT()` is not.77- `Qt::ConnectionType`: `AutoConnection` (default), `DirectConnection`, `QueuedConnection`, `BlockingQueuedConnection`, `UniqueConnection`.78- `QPointer<T>` becomes null when its `QObject` is destroyed; use for observer pointers.79- `emit` is a no-op macro for readability; signals are invoked like functions.80- Copy control: `Q_DISABLE_COPY(Class)` deletes copy ctor/assign; PIMPL uses a `Private *d` (d-pointer), often via `QScopedPointer`/`Q_DECLARE_PRIVATE`.81- Strings: prefer `QStringView` for non-owning read-only string parameters; `QString` is implicitly shared (copy-on-write).82- Implicit sharing (COW): copies are cheap until a mutating (non-const) call triggers a detach; be wary of iterator invalidation across shared copies.83- Threading: `QObject::thread()` reports affinity; `moveToThread(QThread*)` reparents affinity; queued connections marshal calls to the receiver's thread; GUI objects live on the main thread only.84- `SkipEmptyParts` is the Qt 6 `QString::split` flag (replaces the removed `QString::SkipEmptyParts`); the string-based connect enum lives under `Qt::` too.85- Tooling: clazy check levels `level0` (safe, no false positives), `level1` (default), `level2` (may have false positives); run clazy and `clang-tidy` against `compile_commands.json`.