Designing Member Functions for Safe Use
The invariant ties these two members together: the storage that elem refers to must contain exactly the number of doubles recorded in sz.
A Function That Trusts Its Data
Picture a Vector class with an operator[] member function that returns a reference to an element at a given index. That function only means something when the object's own data is sensible. Its job is to look up an element in storage, so it depends on the object's members describing that storage correctly. If the record of how many elements exist disagrees with the storage that actually backs the object, the function has no trustworthy place to look.
What do you think happens?
Suppose the pointer elem leads to storage for 4 doubles, but the count sz says 10. What can operator[] safely do when asked for index 7?
Reveal answer
It cannot do anything safe, because the object's members no longer agree with each other.
The count says index 7 is valid, but the storage only holds 4 doubles. The function has nothing trustworthy to index into, so the result is wrong no matter how carefully operator[] itself is written. The error lives in the data, not in the lookup code.
This section is about the conditions a class depends on regarding its own data. Those conditions have a name, the class invariant, and the rest of this article explains what the name means, who is responsible for the condition, and what goes wrong when it fails.
What the Word Invariant Means
A class invariant is a statement of what is assumed to be true about a class's data, and the class relies on it being true.
The word invariant means unchanging, and the important phrase is at all times the class is meant to be used. An invariant is not a check that runs once and then is forgotten. It is a promise the class makes to itself, so that code inside the class can rely on it whenever that code runs.
For the Vector class, the invariant is a fact about two members: the pointer elem refers to an array holding sz doubles. The class depends on that fact every time an element is accessed, so the fact must stay true for the whole life of the object.
Two Members, One Promise
A Vector object holds two members. The pointer elem leads to a block of storage, and sz records how many doubles that block is meant to hold. The invariant ties these two members together: the storage that elem refers to must contain exactly the number of doubles recorded in sz.
Neither member is meaningful on its own. A pointer with no matching count describes an object whose invariant is false, and so does a count with no matching storage. The invariant is a property of the two members taken together. That is why it cannot be verified by inspecting either member in isolation: you must look at the relationship between them.
Tracing an Object Through Its Lifecycle
An invariant is only useful if it is restored after every change. Follow an object from the moment it is created. It starts in a state where the invariant holds, because the constructor established it. Each member function that is called moves the object forward. A member function may, for a moment, leave the object in a state where the invariant is false. That is the failure the class must not allow to escape to its callers. A correct member function then restores the invariant before it returns.
The lifecycle diagram shows the important rule. The state where the invariant is false is allowed to exist only briefly inside a member function. It must not be visible to the caller once the function exits. If a function returns while the invariant is false, the next caller starts from a broken object and inherits the damage.
Who Establishes and Who Maintains
The invariant has two owners with two different jobs. A constructor is responsible for establishing the invariant, by initializing members and checking that its arguments make sense. This guarantees that every member function can count on the invariant being true from the first moment the object exists.
Member functions are responsible for maintaining the invariant. Each one must make sure the invariant holds again whenever it exits. The split matters. If the constructor fails to establish the invariant, every later function starts from a broken state. If a member function leaves it broken, the next caller inherits the damage.
The Missing Check in the Vector Constructor
The source identifies the gap in the Vector constructor: it initialized the members but did not check that the argument made sense. A size that is negative cannot describe a count of doubles. The following sketch is a generated illustration of one way to close that gap. The class is simplified, leaving out copying and cleanup, because those details are not needed to see the invariant.
- The check runs before any storage is allocated, so a bad size never reaches the members.
- After the check, elem and sz are set from the same value, so the invariant holds from the first moment the object exists.
- Without the check, a negative size would leave sz describing a count that cannot be real.
Constructing a Vector with a bad argument
What happens when a caller constructs a Vector with the size -3, first without the check and then with it?
- 1Without the check
The constructor sets up storage using the value -3 and records -3 in sz. The members now describe an impossible count, so the invariant is false from the moment the object exists.
- 2With the check
The constructor tests the argument first. Because -3 is negative, it rejects the value before allocating storage, so no object is left holding a broken invariant.
- 3Why it matters
Every later member function assumes the invariant is true. A constructor that skips the check hands them a broken starting point that they cannot repair from the inside.
The check closes the gap by stopping invalid data before it can be stored in the members. The invariant is established by the constructor, and the object is never allowed to exist in a broken state.
How a Broken Invariant Spreads
Suppose bad data gets into the object. Follow the chain step by step. The caller asks for an index. operator[] reads elem and sz, but they disagree, so the function is working from false assumptions. Whatever element it returns, or whatever memory it touches, the wrong result comes from the broken relationship between the members rather than from the index itself.
- operator[] trusts that elem holds sz doubles. It does not check this, so it simply indexes elem.
grow_storagereplaces the storage but leaves sz unchanged. The invariant is now false, and the function has returned in that broken state.- The next call to operator[] inherits the broken object and may read past the end of the new storage.
Common Mistakes With Invariants
Common mistake Treating the invariant as a comment
What it looks likeA class says in a comment that elem points to sz doubles, and nothing else in the code checks it.
Why it failsA comment records intent but does not enforce anything. The compiler never reads it, so nothing prevents the members from disagreeing.
Do this insteadLet the constructor establish the invariant and let every member function restore it before it returns.
Common mistake Checking only one member
What it looks likeA constructor validates sz but never ties the storage in elem to that same count.
Why it failsThe invariant is a property of the two members taken together. A pointer with no matching count, or a count with no matching storage, is already broken.
Do this insteadEstablish the relationship between the members, not just the value of one of them.
Common mistake Skipping the constructor argument check
What it looks likeThe Vector constructor accepts any size, so a negative size is stored in sz.
Why it failsThis is the gap the source identifies: the constructor initialized the members without checking that the argument made sense. Every later function then starts from a broken state.
Do this insteadReject invalid arguments before any storage is allocated or any member is set.
Common mistake Returning from a member function with the invariant false
What it looks likegrow_storage replaces the storage and returns without updating the count.
Why it failsThe invariant is allowed to be false only briefly inside a function. If it escapes to the caller, the next caller inherits the damage.
Do this insteadRestore the invariant on every path before the function exits.
Trace the Invariant in a Small Class
A class Buffer has two members: a pointer data that refers to storage for cap ints, and the count cap. A constructor takes a capacity argument and sets both members from it. Name the invariant, say which function establishes it, and identify the one check that is missing from the constructor. Then describe what a member function named resize would have to do to keep the invariant true when it returns.
Hints
- Ask which two members must agree with each other before you name the invariant.
- The constructor is the only function that sets the invariant up for the first time, so look at what it checks before storage is allocated.
- A resize function that replaces the storage must also update the count before it returns.
Key Takeaways
- A class invariant is a statement of what is assumed to be true about a class's data, and the class relies on it being true at all times.
- The invariant is a property of several members taken together, such as elem pointing to storage of exactly sz doubles, so it cannot be verified by looking at one member alone.
- A constructor establishes the invariant by initializing members and checking that its arguments make sense, before any storage is allocated.
- Member functions must maintain the invariant, restoring it before they exit, because a broken invariant that escapes to a caller can make later member functions misbehave.
- An invariant written only in a comment documents intent but does not enforce it, so the class's code must protect the invariant.