Skip to content

← RAII: Objects With Lifetimes step 3 of 4

Medium Primitives

The delete you will forget

Buffer* b = new Buffer(size);
use(b);
delete b;

Three lines, and the middle one can ruin the third. If use throws, the delete never runs. If someone later adds an early return between them, the delete never runs. If someone adds a second exit path and remembers the delete on one but not the other, it runs sometimes.

This is the most common memory leak in C++ and it is never written by someone who forgot that memory must be freed. It is written by someone who freed it in one place, and then the function grew a second exit.

The rule

In modern C++, new and delete should not appear in ordinary code.

Not “use them carefully” — do not write them. They belong inside the implementation of a container or a smart pointer, written once, by someone who is thinking about nothing else at the time.

What replaces them:

Instead of Write
new T(...) then delete a local T — the stack is free
new T[n] then delete[] std::vector<T>
new T you must return std::make_unique<T>(...)
shared ownership std::make_shared<T>(...) — and read Track 4 first

The first row is the one people skip past, and it is the most important. Most heap allocations in C++ code exist for no reason. A local object is allocated by moving a stack pointer, freed by moving it back, and destroyed automatically at the end of the block. If the object does not need to outlive the scope, the scope should own it.

Why [] matters, and why you will not miss it

new T[n] must be freed with delete[], not delete. Getting that wrong is undefined behaviour rather than an error. You will not be diagnosed and the program will usually appear to work.

This is a rule you never have to remember once you stop writing new, which is the point.

A note on how this one is graded

A leak normally shows up as nothing at all: the answer is right, the tests pass, and the memory is gone. That is exactly why leaks survive review, so this problem makes the leak visible. The bucket type counts how many of itself are alive, and the harness reports that number after your function has returned.

So each test checks two things: the histogram, and a live-object count that must be back to zero. A run that returns the right answer and leaves objects alive fails — which is the judgement a real code reviewer would make and a normal test suite would not.

Your task

std::vector<int> histogram(const std::vector<int>& values, int buckets);

Count how many of values fall in each bucket. Bucket i covers values v where v % buckets == i. Negative values are ignored, and any value above 1,000,000 abandons the whole histogram and returns empty. buckets of zero or less returns an empty vector.

The starter allocates the buckets with new[] and frees them on one of its three exits. Rewrite it so that no new or delete appears anywhere, and the leak stops being possible rather than being fixed.