Course outline

C++ / Basic / Lesson 6

Classes, invariants, and composition

Build a small value type that starts valid, exposes meaningful operations, and keeps related state consistent through construction and use.

About 5 min readContent reviewed
On this page

Lesson goals

  • Define constructors, private data, and const member functions.
  • Preserve an invariant through every public operation.
  • Compose standard value members instead of manually managing resources.

Define what a valid object means

A class combines data and operations under an interface. Its invariant is a condition that holds after successful construction and whenever public operations return normally. A stock counter might require a nonnegative quantity; a rectangle might require positive dimensions. Decide the invariant before choosing setters. A public setter for every field can let callers create inconsistent combinations. Prefer operations such as withdraw or rename that express domain changes and validate the entire transition. Encapsulation is useful because it reduces the number of places that must preserve the same rule.

Construct members into a valid state

Constructors initialize members before the constructor body runs. Use a member initializer list or default member initializers to establish their values directly. Members initialize in their declaration order, regardless of the order written in the initializer list. An explicit single-argument constructor avoids accidental implicit conversion from that argument type. The first class deliberately accepts no initial quantity; its default member initializer establishes ten. Its withdrawal checks a complete transition before subtraction, so a rejected operation leaves the state unchanged. All arithmetic here stays in the small representable domain.

cpp Validate before changing stock
#include <iostream>
class Stock {
    int quantity_{10};
public:
    bool withdraw(int amount) {
        if (amount < 0 || amount > quantity_) { return false; }
        quantity_ -= amount;
        return true;
    }
    int quantity() const { return quantity_; }
};
int main() {
    Stock stock;
    std::cout << std::boolalpha << stock.withdraw(4) << " ";
    std::cout << stock.withdraw(8) << " " << stock.quantity() << "\n";
}
Expected output
true false 6

Four units are removed from ten. The next request exceeds six, returns false, and leaves six units available. The chosen initial value is ten.

Distinguish observation from mutation

A const member function can be called on a const object and normally cannot modify its ordinary data members. It communicates an observation operation such as quantity or label. This is a compile-time access restriction rather than a guarantee of thread safety; mutable members and externally referenced objects need separate reasoning. Return small values by value. Returning a reference to an internal string exposes a lifetime and invalidation dependency, so returning an owned string may be preferable at a simple boundary even if a copy is required.

Let member objects manage their resources

Composition means a class contains other objects that implement parts of its behavior. A Team can contain a std::string and a std::vector<std::string> instead of raw owning pointers. Construction, copying, and destruction then follow the members automatically, which is the basis of the rule of zero. In the second program a value parameter accepts an owned member name, and moving that parameter into the vector transfers its available storage when appropriate. The team does not retain a reference to a caller string or require a handwritten destructor.

cpp A class composed of owning standard members
#include <iostream>
#include <string>
#include <utility>
#include <vector>
class Team {
    std::string name_;
    std::vector<std::string> members_;
public:
    explicit Team(std::string name) : name_(std::move(name)) {}
    void add(std::string member) { members_.push_back(std::move(member)); }
    std::string label() const { return name_ + ": " + std::to_string(members_.size()); }
};
int main() {
    Team team{"North"};
    team.add("Mira");
    team.add("Omar");
    std::cout << team.label() << "\n";
}
Expected output
North: 2

The team owns its name and two member names. Its generated destructor releases both container and string resources.

Test operations as state transitions

Test a class through its public interface rather than inspecting private layout. For each operation, specify the state before the call, the return value, and the state afterward. Include rejected requests because preserving an invariant during failure is part of correctness. Separate business rules from presentation so a console, test, or future service can use the same value type. If construction cannot establish a valid state, choose a documented failure policy, such as throwing or a factory result; do not leave a half-valid object that every later method must repair.

Common errors and fixes

A withdrawal creates negative stock.

Why it happens
The operation subtracts before validating the request.
How to fix it
Reject negative and excessive amounts before mutation.
Prevent it next time
Review every public transition against the invariant.

Member values depend on misleading initializer order.

Why it happens
Initialization follows declaration order instead.
How to fix it
Declare dependencies first and match initializer order.
Prevent it next time
Enable reorder warnings and avoid circular initialization.

A class destructor duplicates standard cleanup.

Why it happens
Owning containers were treated like unmanaged pointers.
How to fix it
Let string and vector members destroy themselves.
Prevent it next time
Prefer the rule of zero for ordinary value classes.

Check your understanding

1. Using Stock, predict withdrawing zero, negative two, and ten from a fresh object. Give both return values and final quantities.

Show worked solution

A fresh Stock contains ten. Withdrawing zero returns true and leaves ten. Withdrawing negative two returns false and leaves ten. Withdrawing ten then returns true and leaves zero. Requests are evaluated sequentially; rejected operations never change quantity. Add a withdrawal of one afterward to confirm it returns false while the quantity remains zero.

2. Correct a design in which a Team stores const std::string& for a caller-provided temporary team name. Explain ownership and the generated special members.

Show worked solution

Store std::string name_ by value as in the example. A temporary argument can then initialize owned storage whose lifetime matches the Team. A generated copy copies the string and vector values, and the generated destructor releases them. A reference member would preserve a borrowing dependency and could dangle after the temporary dies; copying that reference would not create ownership.

Key takeaways

  • Construct valid objects and preserve their invariants.
  • Prefer domain operations over unrestricted field setters.
  • Compose owning value members and use the rule of zero.

Official references