Skip to content

← Const Correctness and Class Design step 3 of 3

Medium Primitives

The handle you left in the door

A class exists to hold something true. Ledger keeps a running total that always equals the sum of its entries; SortedList stays sorted; Connection is either open or closed and never both. That statement is the class’s invariant, and it is the only reason to have a class rather than a struct with some functions near it.

An invariant is only true if it cannot be broken from outside. Which makes the public section a promise about what callers are able to do:

Anything reachable from outside will eventually be used from outside. Not because your colleagues are careless, but because it is there, it compiles, and it was the shortest path to whatever they needed that day.

Where the hole usually is

Not in a public data member — those get caught in review. It is in the accessor:

std::vector<int>& entries() { return entries_; }   // a hole

It reads like a getter. It hands out a non-const reference to the object’s own storage, so every caller can push_back, clear, erase and sort the internals while the class goes on believing its total is correct. The _ suffix and the private: above it bought nothing.

const std::vector<int>& entries() const { return entries_; }   // a view

Two consts, and the accessor is now what it looked like all along.

The rule of thumb

Ask If yes
Does a caller need to read this? return const T&
Does a caller need to change this? give them the operation, not the data
Neither? it is private, and stays private

The middle row is the one that takes discipline. A caller who wants to append an entry does not need your vector; they need add(value) — a function that can update the total, reject a bad input, and keep working when the storage changes to something else next year. Every operation you expose is a decision you can revisit. Every piece of data you expose is one you cannot.

And the smaller surface is also the cheaper one

Everything public is something you must keep working. A class with four public functions has four things to get right in the next refactor; one that also returns a mutable reference to a std::vector has committed to the vector.

Your task

Ledger claims that total() is the sum of its entries. It is not, because entries() hands out the vector and record uses it.

  1. Close the hole: entries() should return something a caller can only read.
  2. Give record the operation it actually needed — an add on Ledger that keeps the total honest.
  3. Update record to use it. It is the caller that was reaching in, and once the hole is closed it will not compile as written.

The harness compares total() against the real sum of the entries.