C++ / a working model

81 / 163   ·   C++11   ·   12 min

Class invariants: keep illegal states out of objects

Keep this sentence

A constructor does more than fill members; it is responsible for establishing the object’s promise. Mutating operations must maintain that same promise. Using a capacity-capped stock as an example, putting range checks inside the type and leaving the old value unchanged on failure is more reliable than requiring every caller to check conscientiously.

In this lesson
  1. Write the rules first, then the members
  2. Do not mutate before failure: make each operation a transaction
  3. Migrating from textbook hand-written containers to modern code
  4. Example
  5. Exercise
READING EVIDENCE / Full text read

The C++ Programming Language

The complete body text of chapters 1–44 (print pages 3–1279) has been read in sections, including code, tables, and end-of-chapter advice, plus the prefaces of the various printings; private text L140–59048 covers this continuously. Subsequently, 126 already-located original PDF figures, formulas, and surrounding pages were checked directly, filling in visual content such as object layouts, inheritance arrows, matrices, streams, and concurrency. “full” means the body text and the identified substantial figures have been read; it does not mean every one of the 1,366 PDF pages was collated page by page. The index was only sampled for navigation, not read entry by entry. The original book is a C++11 baseline; C++20 differences are listed separately.

Edition, actual reading range, and original sources →

Write the rules first, then the members

A stock object’s promise is 0 <= used <= capacity. If the two integers are public, callers can modify them separately, and any assignment may produce an illegal combination. The value of a class is not moving functions inside braces; it is concentrating legal states and allowed changes behind one boundary.

The constructor checks initial values; the read interface does not change state; a reserve operation checks remaining capacity before committing. Thus every successful construction and mutation maintains the same invariant. A failed parameter check is not “stock is zero”; it means no new legal operation result was produced. The example reports this explicitly via an exception.

Do not mutate before failure: make each operation a transaction

When reserving, do not add first and check afterward. For signed integers, the addition may already have overflowed, and a later check cannot undo it; for unsigned integers, wraparound can also hide the error. When the invariant is known to hold, comparing amount > capacity - used does not compute a dangerous sum.

Only if the check passes is the addition performed. For this integer-only operation, the exception path guarantees the old state is completely unchanged; that is not a language guarantee automatically enjoyed by every member function, but an interface property obtained by arranging statement order. Complex resource mutations should likewise prepare first and commit later.

Migrating from textbook hand-written containers to modern code

The book explains construction, destruction, and copying through resource handles, exceptions, and special members; it demonstrates mechanisms, which does not mean business code should generally hand-write dynamic arrays. This stock class has no resource ownership, so it need not declare a destructor or copy functions; default value semantics already suffice.

These design principles still apply in C++20. assert is for example checks and does not replace runtime validation of external input; you must not let illegal arguments slip into the object just because assertions are disabled in release builds. Early printings of the book have author errata for sample code; this site’s programs do not copy its container implementations.

Pitfalls

  • Doing used += amount first and then checking the limit may already have overflowed; you must compare remaining capacity first.
  • Do not use assertions to handle illegal arguments from outside; assertions can be disabled, but the interface promise cannot disappear.

Run an example

Minimum C++11 · complete program · Download .cpp

#include <cassert>
#include <stdexcept>
class Stock {
    int capacity_;
    int used_;
public:
    Stock(int capacity, int used) : capacity_(capacity), used_(used) {
        if (capacity < 0 || used < 0 || used > capacity)
            throw std::invalid_argument("invalid stock");
    }
    int used() const noexcept { return used_; }
    void reserve(int amount) {
        if (amount < 0) throw std::invalid_argument("negative amount");
        if (amount > capacity_ - used_) throw std::out_of_range("no capacity");
        used_ += amount;
    }
};
int main() {
    Stock s(10, 3);
    s.reserve(7);
    assert(s.used() == 10);
    bool rejected = false;
    try { s.reserve(1); } catch (const std::out_of_range&) { rejected = true; }
    assert(rejected && s.used() == 10);
    rejected = false;
    try { Stock invalid(2, 3); } catch (const std::invalid_argument&) { rejected = true; }
    assert(rejected);
    Stock copy = s;
    copy.reserve(0);
    assert(copy.used() == s.used());
}

Compile locally

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-cpp-programming-language.cpp -o example && ./example

Expected result

Expected: exit 0, no output; every assert holds.

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

Add release(amount), guaranteeing that releasing too much fails and the old value is unchanged.

Show a reference answer

Implement if (amount < 0) throw std::invalid_argument("negative amount"); if (amount > used_) throw std::out_of_range("too much"); used_ -= amount;. After release(4) on a full object, used is 6; a further release(7) should throw and still be 6. Comparing first then subtracting simultaneously guarantees non-negativity and that failure does not mutate.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog