qt-cpp-review
Review Qt6 C++ code for correctness, memory ownership, and idiomatic use of QObject, signals/slots, and implicit sharing.
SKILL.mddocs/skills/qt-cpp-review/SKILL.md
GitHubWhen 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
QObjectidentity: every class deriving fromQObjectthat declares signals, slots, orQ_PROPERTYmust have theQ_OBJECTmacro and be reachable bymoc. Decision point: ifQ_OBJECTis missing, signals/slots andqobject_castsilently break — flag it before anything else. - Trace ownership: confirm each heap
QObjecthas a parent or a clear owner. Decision point: parented object → do notdeleteit 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-basedSIGNAL()/SLOT()form. CheckQt::ConnectionTypefor cross-thread cases. - Look for dangling access: raw
QObject*held across time should beQPointerso it nulls on destruction. - Review value semantics: containers and
QStringpassed by value where aconst T&(orQStringViewfor read-only string args) would avoid copies. - Check copy control: classes managing resources or identity should use
Q_DISABLE_COPYor 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 againstcompile_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
QObjectyourself 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::DirectConnectionslot runs in the emitter's thread; touching GUI there is undefined. - Dangling raw pointer. A cached
QObject*outliving its target crashes;QPointernulls 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 withoutQ_DISABLE_COPYduplicates 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_OBJECTin the private section is mandatory for signals/slots,Q_PROPERTY,qobject_cast, and runtime type info; the class must be processed bymoc.- 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; stringSIGNAL()/SLOT()is not. Qt::ConnectionType:AutoConnection(default),DirectConnection,QueuedConnection,BlockingQueuedConnection,UniqueConnection.QPointer<T>becomes null when itsQObjectis destroyed; use for observer pointers.emitis a no-op macro for readability; signals are invoked like functions.- Copy control:
Q_DISABLE_COPY(Class)deletes copy ctor/assign; PIMPL uses aPrivate *d(d-pointer), often viaQScopedPointer/Q_DECLARE_PRIVATE. - Strings: prefer
QStringViewfor non-owning read-only string parameters;QStringis 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. SkipEmptyPartsis the Qt 6QString::splitflag (replaces the removedQString::SkipEmptyParts); the string-based connect enum lives underQt::too.- Tooling: clazy check levels
level0(safe, no false positives),level1(default),level2(may have false positives); run clazy andclang-tidyagainstcompile_commands.json.