Skip to content

← RAII: Objects With Lifetimes step 3 of 4

Medium Primitives

Let a vector own the histogram buckets

Manual cleanup can be skipped when control leaves a function early:

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

If use throws and an outer handler catches the exception, the delete expression is never reached. The pointer itself does not own or destroy the Buffer. A return inserted before delete would have the same cleanup problem.

Prefer a local object, container or smart pointer that owns the resource. Its destructor handles cleanup when its scope ends normally or is unwound. Low-level allocation code may still need new and delete, but ordinary callers should usually work with an owning type.

Choose ownership to match the task

Need Typical choice
An object whose lifetime fits a scope A local object
A runtime-sized sequence std::vector<T>
A dynamically allocated object with one owner std::unique_ptr<T>, usually created with std::make_unique
An object whose lifetime genuinely needs shared ownership std::shared_ptr<T>

Returning an object does not by itself require dynamic allocation; returning by value is often suitable. A local object may also allocate internally, as a vector does for its elements. Automatic cleanup is an ownership property, not a promise that construction is free or that no allocation occurs.

When manually managing an ordinary new T[n] array, use the matching delete[] form. Using scalar delete on that array has undefined behavior. Tools may diagnose a mismatch, but a successful compilation does not prove it safe.

Your task

Rewrite:

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

Use a local std::vector<Cell> to own the bucket counters. Keep Cell and live_cells unchanged, and remove the new[] and delete[] expressions from histogram. The supplied = delete declarations disable copying; leave them in place. They are unrelated to deallocating an object.

Apply these rules:

  • If buckets <= 0, return an empty vector before doing remainder arithmetic.
  • Otherwise, begin with buckets counters, each zero.
  • Ignore negative values.
  • If any value is greater than 1,000,000, discard the entire histogram and return empty. Exactly 1,000,000 is allowed.
  • For each remaining value v, increment bucket v % buckets.
  • Return the counts in bucket order, from index 0 to buckets - 1.

Inputs and each resulting count fit int; test bucket counts are small enough for the allocation. Leave values unchanged. Empty input with three buckets returns [0, 0, 0], not an empty vector.

For values = [-1, 0, 1, 2, 5] and buckets = 3, ignore -1 and count the remaining remainders 0, 1, 2 and 2. The result is [1, 1, 2].

What the harness checks

Every Cell constructor increments live_cells; its destructor decrements it. The harness returns [counts, live_cells] after histogram has returned, and live_cells must be zero. For the example above, expect [[1, 1, 2], 0].

In the starter, buckets <= 0 returns before allocation. The normal path deletes the array, but finding a value above the limit returns after allocation without deleting it. For [1, 2000000, 3] and two buckets, the starter returns an empty histogram but leaves two Cells alive. The required output is [[], 0].

A local vector destroys its Cells on either return path and also during exception unwinding. The live counter makes this specific ownership mistake visible; it does not detect every possible memory leak.

Construct the counter vector with a size argument in parentheses:

std::vector<Cell> counts(static_cast<std::size_t>(buckets));

This constructs that many Cells, whose n member starts at zero. Braces can prefer an initializer-list constructor when one is viable, as in std::vector<int>{3}, which contains one value. The result depends on the element type and available constructors, so braces do not universally mean an element list. Parentheses express the intended count clearly here.