Qt Implicit Sharing: Why Copying QString, QList and QMap Is Cheap Until You Write
Qt's implicit sharing makes passing QString, QList and QMap cheap until the first write. See when detach happens, which reads trigger it by accident, and why threads still need explicit locking.
03 Dec 2025, 06:42 UTC

Passing a large QStringList or QMap from UI code to a worker is a common place to hesitate. Value semantics are safer, but a deep copy on every call sounds expensive. Qt's implicit sharing makes the pass cheap and defers the copy until the first write. The practical rule: assignment is cheap, mutation is where you pay, and threads are never handled for you.
What implicit sharing defers
QString, QList and QMap are implicitly shared. The object on the stack is small: it holds a pointer to a heap-allocated data block that carries a reference count. Copying the object increments the count and points at the same block. No elements are duplicated.
Detaching is the write path. When you mutate an instance whose data block is still referenced elsewhere, Qt allocates a fresh block, copies the elements once, then mutates the private copy. Reads never detach.
| Operation | What happens | Relative cost |
|---|---|---|
| Copy-construct or assign | Reference count incremented, block shared | Small, atomic |
| Read-only access | Same block used | No allocation |
| First write while shared | Detach: new block plus one deep copy | Proportional to element count |
| Write while unshared | In-place mutation | No allocation |
A worked example
Build this in any Qt project (Qt 5 or Qt 6) and run the binary from the build directory, which needs write permission. The snippet is illustrative and has not been executed for this article.
QList<QString> original;
original << "a" << "b" << "c";
QList<QString> copy = original; // shares the block; count increases
copy[0] = "x"; // detach: copy gets its own block
Q_ASSERT(original.at(0) == "a"); // original is untouched
Q_ASSERT(copy.at(0) == "x");
Expected check: both assertions pass. If the first one fails, the container was already detached before the write — something earlier in the function called a non-const method on it.
The detach you did not ask for
Non-const access detaches. A range-based for loop over a non-const Qt container calls the non-const begin(), which detaches the whole container even if the loop body only reads. The same applies to operator[] on a non-const container and to passing a container to a function that takes it by non-const reference.
Use qAsConst() (Qt 5.7 and later) or std::as_const() in the loop, or iterate a const reference:
const QList<QString> &view = original;
for (const QString &s : view) { /* no detach */ }
for (const QString &s : qAsConst(original)) { /* no detach */ }
This is the most useful habit when profiling: a read-only loop that shows up as allocation is usually a missing const.
API design consequences
Because copies are cheap, Qt-style APIs can take containers by value and return them by value. A function that may need to modify its argument can take it by value and mutate the private copy; the caller's object stays shared until the callee actually writes. For read-only parameters, take a const reference and avoid detaching entirely.
For your own types, the same mechanism is available through QSharedData and QSharedDataPointer. Note the distinction: QSharedDataPointer detaches on non-const access, while QExplicitlySharedDataPointer only detaches when you ask it to.
Where the model breaks down
Reference counting is not free. Atomic increments in tight loops, or frequent copies that cross thread boundaries, can become measurable. The detach copy itself is proportional to element count, so a single write to a shared million-element list costs a full copy.
Implicit sharing provides no synchronization. Two threads holding references to the same block while one of them writes is a data race; the detach does not make it safe. Either give each thread its own detached copy before handing it over, or guard the shared object with QMutex or QReadWriteLock. Queued signal/slot connections copy arguments, but that copy is shallow until someone writes, so the receiving thread can still share a block with the sender.
Accidental mutation is silent in the other direction too. Changing one instance after it has been passed elsewhere detaches only that instance; the other side keeps the old value. That is correct value semantics, but it hides bugs when the author expected a shared update.
Checking your assumptions
Two practical checks:
- Write the assign-mutate-assert test above for each container type you rely on. It takes a minute and settles the question for your Qt version.
- For asynchronous code, detach deliberately before crossing a thread or event-loop boundary — for example by taking a copy and writing to it once, or by passing a const reference into a function that copies into a local.
Qt debug builds can print reference-count information for QSharedDataPointer-based types, but that output is verbose and is not a substitute for the assertion test. Treat any performance claim about implicit sharing as something to measure in your own build rather than assume.
Keep value semantics for clarity, add const where you only read, and never treat implicit sharing as thread safety.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.