Programming Principles and Practice Using C++

Programming Principles and Practice Using C++ · Bjarne Stroustrup ·1274 pages

Stroustrup's C++ pedagogy textbook: class design from struct to class with invariants and pre/postconditions, deep copy semantics (copy constructor + assignment), vector internals (sz/elem/space capacity model, amortized push_back), RAII with exception safety guarantees (basic/strong/no-throw), grammar-to-recursive-descent-parser translation, and embedded systems C++ constraints (no new/delete/exceptions in hard real-time, pool allocators, bitfields).

Capabilities (8)
  • Design classes with explicit invariants and enforce them in constructors by throwing exceptions on invalid state
  • Write pre/postconditions as comments and runtime checks to catch usage errors at the boundary
  • Implement deep copy via copy constructor (const ref arg) and copy assignment (allocate new → copy → delete old → swap)
  • Implement vector internal representation with sz/elem/space and amortized O(1) push_back via doubling
  • Apply RAII: tie resource lifetime to object lifetime so destructor frees resources even when exceptions are thrown
  • Reason about exception safety guarantees: basic (no leaks), strong (all-or-nothing), no-throw (built-ins)
  • Turn a BNF grammar into a recursive descent parser where each rule is a function and putback() handles lookahead
  • Write hard real-time C++ without new/delete/exceptions using pool allocators and fixed-size stack arrays
How to use

Install this skill and Claude can design classes with explicit invariants enforced in constructors, audit and fix shallow-copy bugs, translate BNF grammars into recursive descent parsers, and rewrite heap usage with RAII wrappers or pool allocators for embedded systems

Why it matters

Stroustrup's pedagogy builds the foundational mental models behind every modern C++ best practice — why invariants belong in constructors, why the rule of three exists, and why RAII is the only reliable resource management strategy under exceptions

Example use cases
  • Refactoring a BankAccount struct into a class with an enforced non-negative-balance invariant that throws a nested exception type on violation
  • Identifying and fixing a class that uses compiler-generated shallow copy on a heap-allocated member by implementing a correct deep-copy constructor and assignment operator
  • Translating a BNF grammar for a calculator with +, -, *, /, and parentheses into a complete recursive descent parser with Token_stream and putback() support

C++ Programming Principles Skill

Class Design: Evolving from struct to class

Invariant-based design

// If you can't state an invariant, use a struct (plain data)
// If you can state an invariant, use a class and enforce it in the constructor

class Date {
public:
    class Invalid { };                       // exception type, nested in class
    Date(int y, int m, int d);               // constructor checks invariant
    void add_day(int n);
    int month() const { return m; }          // inline: tiny function, called often
    // ... more accessors
private:
    int y, m, d;
    bool check() const;                      // private helper: test validity
};

Date::Date(int yy, int mm, int dd)
    : y(yy), m(mm), d(dd)                   // member initializer list (prefer over assignment)
{
    if (!check()) throw Invalid();           // enforce invariant on construction
}

bool Date::check() const {
    if (m < 1 || 12 < m) return false;
    // ... validate day range, leap years ...
    return true;
}

Key rules:

  • State the class invariant in a comment: what constitutes a valid value
  • Enforce it in every constructor — if invariant holds after construction and member functions maintain it, the object is always valid
  • Make member functions const when they don’t change state
  • Inline only tiny functions (≤1–2 expressions) — large inline functions increase recompile burden
  • Member function bodies inside class definition → implicitly inline + recompile on change

Pre- and post-conditions

// Pre-condition: what a function requires of its arguments
// Post-condition: what a function guarantees about its return value
int area(int length, int width)
// pre-condition: length > 0, width > 0
// post-condition: returns a positive value that is the area
{
    if (length <= 0 || width <= 0) error("area() pre-condition violated");
    int a = length * width;
    if (a <= 0) error("area() post-condition violated");  // catches overflow
    return a;
}

// Rule: check pre-conditions unless there's a proven performance reason not to
// Rule: always document pre-conditions in comments — even if you can't check them

Copy Semantics

The problem with default (memberwise) copy

class vector {
    int sz;
    double* elem;    // pointer to heap-allocated array
    // ...
};

// Default copy copies the pointer, not the pointed-to data:
vector v2 = v;  // v.elem == v2.elem → same array!
// Problem: modifying v[0] also modifies v2[0]
// Problem: both destructors call delete[] on the same memory → double free

Deep copy: copy constructor + copy assignment

class vector {
    int sz;
    double* elem;
    void copy(const vector& arg) {         // copy elements; sizes must match
        for (int i = 0; i < arg.sz; ++i)
            elem[i] = arg.elem[i];
    }
public:
    // Copy constructor: called on initialization
    vector(const vector& arg)              // const ref — don't copy the arg!
        : sz(arg.sz), elem(new double[arg.sz])
    {
        copy(arg);
    }

    // Copy assignment: called on assignment (must handle old value)
    vector& operator=(const vector& a) {
        double* p = new double[a.sz];      // 1. allocate new space
        for (int i = 0; i < a.sz; ++i)
            p[i] = a.elem[i];             // 2. copy elements
        delete[] elem;                     // 3. free old memory
        elem = p;                          // 4. swap in new memory
        sz = a.sz;
        return *this;                      // 5. return self-reference
    }
};

Copy terminology:

  • Shallow copy: copies the pointer (pointer, reference) — both point to same data
  • Deep copy: copies the pointed-to data — vectors, strings use deep copy

Rule: Allocate new space before deleting old in assignment. Self-assignment (v = v) must work correctly.


Vector Internal Representation

class vector {
    int sz;      // number of elements (size)
    double* elem;// pointer to first element
    int space;   // total allocated slots (capacity = sz + free slots)
public:
    vector() : sz(0), elem(nullptr), space(0) {}

    void reserve(int newalloc) {
        if (newalloc <= space) return;       // never shrink allocation
        double* p = new double[newalloc];
        for (int i = 0; i < sz; ++i) p[i] = elem[i];  // copy elements
        delete[] elem;
        elem = p;
        space = newalloc;
    }

    void push_back(double val) {
        if (sz == space) reserve(space == 0 ? 8 : 2 * space);  // amortize: double
        elem[sz] = val;
        ++sz;
    }

    void resize(int newsize) {
        reserve(newsize);
        for (int i = sz; i < newsize; ++i) elem[i] = 0;  // init new elements
        sz = newsize;
    }
};

Key properties:

  • size() = number of elements; capacity() = allocated slots
  • push_back amortizes cost by doubling capacity: O(1) amortized
  • Never store in a pointer inside a function; use RAII containers

RAII and Exception Safety

The problem: explicit new/delete + exceptions = leaks

void suspicious(int s, int x) {
    int* p = new int[s];    // acquire resource
    vector<int> v;
    // ...
    if (x) p[x] = v.at(x); // v.at() might throw out_of_range
    // ...
    delete[] p;             // NEVER REACHED if exception thrown → leak!
}

RAII: Resource Acquisition Is Initialization

void f(int s) {
    vector<int> p(s);    // constructor acquires memory
    vector<int> q(s);    // constructor acquires memory
    // ...
    // Destructors for p and q called on exit — even if exception thrown
    // This is RAII: resource tied to object lifetime
}

// Rule: use vector (not new/delete) for dynamic storage within a scope
// Resources: memory, file handles, sockets, locks — all managed this same way

Exception safety guarantees

Basic guarantee (minimum):  No resources leaked if exception thrown.
                            Observable state may have changed.
                            All standard library code provides this.

Strong guarantee (ideal):   If exception thrown, all observable values are
                            the same as before the call. "All-or-nothing."

No-throw guarantee:         Operation cannot throw. All built-in C++ operations
                            (integer arithmetic, pointer ops) provide this.
                            Required to implement basic + strong guarantees.

auto_ptr (C++03) / unique_ptr (C++11) pattern

// When you must put a heap object into a function that could throw:
vector<int>* make_vec() {
    auto_ptr<vector<int>> p(new vector<int>);  // p owns the pointer
    // ... fill *p; may throw ...
    return p.release();   // give up ownership, return raw pointer to caller
    // If exception: p's destructor deletes the vector → no leak
}

RAII base class pattern (vector_base)

// Separate memory management from object management:
template<class T, class A>
struct vector_base {
    A alloc;
    T* elem;
    int sz, space;
    vector_base(const A& a, int n)
        : alloc(a), elem(a.allocate(n)), sz(n), space(n) {}
    ~vector_base() { alloc.deallocate(elem, space); }  // cleanup guaranteed
};

template<class T, class A = allocator<T>>
class vector : private vector_base<T,A> {
    // ... vector operations
    void reserve(int newalloc) {
        vector_base<T,A> b(alloc, newalloc);   // allocate new space
        for (int i = 0; i < sz; ++i)
            alloc.construct(&b.elem[i], elem[i]);  // copy (may throw)
        for (int i = 0; i < sz; ++i)
            alloc.destroy(&elem[i]);
        swap<vector_base<T,A>>(*this, b);      // swap representations
        // b's destructor frees old memory — even if copy threw
    }
};

Grammar → Recursive Descent Parser

Pattern: map BNF grammar rules directly to functions

Grammar (calculator):
  Expression:
    Term
    Expression "+" Term
    Expression "-" Term
  Term:
    Primary
    Term "*" Primary
    Term "/" Primary
  Primary:
    Number
    "(" Expression ")"
// Each grammar rule becomes a function:
double expression();   // handles + and -
double term();         // handles * and /
double primary();      // handles numbers and parentheses

// Token stream: reads ahead by 1
class Token_stream {
    bool full;
    Token buffer;
public:
    Token get();
    void putback(Token t) { buffer = t; full = true; }
};

Token_stream ts;

double primary() {
    Token t = ts.get();
    switch (t.kind) {
    case '(':
    {
        double d = expression();
        t = ts.get();
        if (t.kind != ')') error("')' expected");
        return d;
    }
    case '8':          // numeric literal kind
        return t.value;
    default:
        error("primary expected");
    }
}

double term() {
    double left = primary();
    Token t = ts.get();
    while (true) {
        switch (t.kind) {
        case '*': left *= primary(); t = ts.get(); break;
        case '/':
        {   double d = primary();
            if (d == 0) error("divide by zero");
            left /= d; t = ts.get(); break; }
        default:
            ts.putback(t);  // return token we didn't use
            return left;
        }
    }
}

double expression() {
    double left = term();
    Token t = ts.get();
    while (true) {
        switch (t.kind) {
        case '+': left += term(); t = ts.get(); break;
        case '-': left -= term(); t = ts.get(); break;
        default:  ts.putback(t); return left;
        }
    }
}

Key insight: Higher-precedence operators are parsed deeper in the call stack. expression() calls term() which calls primary() — this encodes precedence naturally.


Embedded Systems C++

Unpredictable operations to avoid in hard real-time

// AVOID in hard real-time (unpredictable timing):
new / delete          // heap fragmentation, unpredictable allocation time
string, vector, map   // use heap internally
exceptions            // unpredictable unwind path
dynamic_cast          // implementation-dependent cost

// SAFE (predictable):
int, double, pointer arithmetic
virtual function calls
static-sized arrays on stack

Pool allocator (predictable memory)

// Allocate a large block once; hand out fixed-size chunks
class Pool {
    struct Chunk { Chunk* next; };
    Chunk* head;
    char storage[N * sizeof(T)];   // fixed-size storage
public:
    Pool() : head(nullptr) {
        for (int i = 0; i < N; ++i) {
            Chunk* p = reinterpret_cast<Chunk*>(&storage[i * sizeof(T)]);
            p->next = head;
            head = p;
        }
    }
    void* allocate() {
        if (!head) throw bad_alloc();
        void* p = head;
        head = head->next;
        return p;
    }
    void deallocate(void* p) {
        Chunk* c = static_cast<Chunk*>(p);
        c->next = head;
        head = c;
    }
};

Bit manipulation (hardware registers)

// Bitset for flags (type-safe, bounds-checked)
#include <bitset>
bitset<8> flags;
flags.set(3);      // set bit 3
flags.reset(3);    // clear bit 3
flags[3];          // access bit 3
flags.to_ulong();  // convert to integer

// Bit fields (struct members mapped to specific bit positions)
struct PPN {          // page table entry
    unsigned int pfn : 22;    // page frame number (22 bits)
    unsigned int : 3;         // unused (3 bits)
    unsigned int cca : 3;     // cache coherency algorithm (3 bits)
    unsigned int nonreorder: 1;
    unsigned int ro : 1;
    unsigned int dirty : 1;
    unsigned int valid : 1;
};
PPN& p = reinterpret_cast<PPN&>(reg);  // overlay struct on hardware register
p.valid = 1;

// Bit operations
const int bit3 = 1 << 3;
flags |= bit3;             // set bit 3
flags &= ~bit3;            // clear bit 3
bool b = (flags >> 3) & 1; // test bit 3

Coding standards for safety-critical C++

Avoid:
  - No global variables with complex constructors (init order undefined)
  - No exceptions in hard real-time code
  - No dynamic allocation after startup
  - No recursion (stack depth unpredictable)

Prefer:
  - Fixed-size arrays over vectors (known memory footprint)
  - Stack-allocated objects over heap
  - assert() for invariants in debug builds, remove for release
  - Every resource has exactly one owner (RAII)