43 / 163 · C++14 · 10 min
Memory leaks: from responsibility to evidence
A leak is first a resource that was not reclaimed at the agreed time, not a simple observation of whether process memory dropped. Cover exception paths with RAII, then combine LeakSanitizer allocation stacks with a repeatable workload to distinguish orphaned allocations, reference cycles, unbounded caches, and allocator retention.
In this lesson
Locate the lifetime contract first
A typical leak is a dynamic allocation that loses its only release path, for example a raw pointer being overwritten or an exception skipping a manual delete. There is also a business leak: the object remains reachable from a global container, but a cache grows without bound or expired tasks are never removed. A detector's judgment that an object is reachable is not the same as the business having accepted that it should live forever.
shared_ptr can also leak through a cycle, so you cannot search only for raw new. During review, follow ownership to find who should release whom, and inspect callback captures, subscription registrations, and global caches. The real fix is a clear responsibility and exit condition, not blindly deleting every address before the program ends.
Get allocation evidence with dynamic tools
On supported platforms, compile and link with clang++ -std=c++14 -g -O1 -fsanitize=address -fno-omit-frame-pointer example.cpp -o example, then run a repeatable workload. ASan integrates LSan on some platforms; enable it when needed through ASAN_OPTIONS=detect_leaks=1; you can also use -fsanitize=leak alone.
When reading a report, keep the first allocation call stack and narrow it to the create, transfer, and exit paths. Dynamic detection covers only paths that actually ran; no report is not proof of no leaks. Exceptions, cancellation, and early returns often expose missing cleanup more readily than the success path, so they must be included in reproduction, rather than running a single normal completion.
Confirm reclamation with observable state
The example creates a Worker on the failure path and on the success path, relies on RAII for destruction, then asserts that the live count returns to zero. It does not leak on purpose and does not treat a failing run as a teaching prerequisite. The count can show construction and destruction balance for this kind of object, but it is not whole-process leak detection and should still be combined with tool reports.
Memory monitoring is also affected by allocator caches, retained capacity, and fragmentation. After a vector is cleared, size returns to zero but capacity can remain; resident set size seen by the operating system may also lag. First prove whether the object is still owned, then analyze why the underlying storage is retained, avoiding repeated allocation and worse performance introduced just to “bring the curve down.”
Pitfalls
- Using the operating system reclaiming address space after process exit as a cleanup strategy for a long-running service will hide unbounded growth during the run.
- Ignoring or suppressing every detector report cannot prove a fix; keep the reproducing input and confirm the report disappears on the same path.
Run an example
Minimum C++14 · complete program · Download .cpp
#include <cassert>
#include <memory>
#include <stdexcept>
struct Worker {
static int alive;
Worker() { ++alive; }
~Worker() { --alive; }
};
int Worker::alive = 0;
void run(bool fail) {
auto worker = std::make_unique<Worker>();
if (fail) throw std::runtime_error("cancelled");
}
int main() {
bool caught = false;
try { run(true); }
catch (const std::runtime_error&) { caught = true; }
assert(caught && Worker::alive == 0);
run(false);
assert(Worker::alive == 0);
}
Compile locally
g++ -std=c++14 -Wall -Wextra -Wpedantic -pthread memory-leaks.cpp -o example && ./exampleExpected result
Expected: exit 0, no output; every assert holds.
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
After the service processes each batch of tasks, RSS rises, but every Worker has already been destroyed. Can you conclude there is no leak?
Show a reference answer
No. The Worker count covers only one object type; other allocations, caches, or reference cycles may still grow. Repeat a fixed workload, combine LSan or heap-analysis allocation stacks, and check global container sizes and retained capacity. Only if business-owned amounts are stable and RSS stabilizes after warmup is there evidence to investigate allocator caches further; do not conclude from destructor counts or a single drop in memory alone.
Check the sources
Drafts and official chapters change. The version mark is only the example’s minimum.