Why this matters
A program is correct only when its data makes sense. A bank balance should not be negative. A person's age should not be 500. A hotel stay should not end before it starts. If a bad value gets into an object, the rest of the program may run without any error and produce wrong results, and the mistake shows up far away from where it began.
Validation is the act of checking data and refusing it when it breaks a rule. This lesson shows how to keep an object valid from the moment it is built and after every change. The approach is to check in the constructor, check in each setter, and keep the fields private so no other code can skip those checks. The earlier lesson on classes introduced constructors and setters. This lesson is about what the constructor and setters must check.
What an invariant is
An invariant is a rule that is always true about an object, from the moment it is built until the moment it is destroyed. The word means "does not change", but an invariant is about the object's state being valid at every point you can observe it. Some examples:
- A person's name is not empty.
- A person's age is between 0 and 150.
- A reservation's check-out day is after its check-in day.
These are invariants because each one is a promise the object makes to its users. Code that uses a Person can rely on the name being non-empty and the age being in range. It does not need to re-check every time it reads the age. That is the benefit: the checking happens in one place, and the rest of the program can trust the data.
An invariant that is not enforced is just a hope. The rest of this lesson shows how to enforce it.
The three places a rule is checked
A valid object needs checks in three places.
- The constructor. The object is born valid, or it is not born at all.
- Every setter, which is any method that changes a field. A setter must not allow a change that breaks a rule.
- Private fields. If other code can write to a field directly, it can skip every check. Keep the fields private, so the only way to change them is through the checked methods.
Each of these is needed. If the constructor checks but the setter does not, a caller can build a valid object and then break it. If the setter checks but the fields are public, a caller can assign to the field and skip the setter.
Step one: check in the constructor
The constructor is the first chance to enforce a rule. If the rule fails, the constructor cannot finish building the object, so it reports the problem by throwing an exception. A throw stops the current function and passes an error object up to the caller. The caller catches it with a try block. Exceptions have their own lesson later in this module. For now, read throw std::invalid_argument("...") as "refuse to build this object, and say why".
Here is a Person class with a name and an age. The constructor does not store anything until the checks pass.
Expected output:
created Asha, age 34
rejected: name must not be empty
rejected: age must be between 0 and 150
rejected: age must be between 0 and 150
setter rejected: age must be between 0 and 150
still 20Look at the constructor first. It does no checking itself. It hands the name to set_name and the age to set_age. The rules live in the setters, so the constructor and the setters can never disagree about what is valid. The constructor reuses the setters on purpose.
There is one detail to notice. set_name takes its argument by value, and it moves the value into name_ only after the check passes. If the check fails, the object is not changed at all. This is the usual order: check first, then change anything.
Each call to try_make builds a Person inside a try block. The first call succeeds. The other three each throw inside the constructor, the exception skips the rest of the try block, and the catch prints the message. The caller never gets a half-built Person, because the object's constructor did not finish.
Step two: check in every setter
The constructor is only half the job. After an object is built, other code calls setters to change it. Each setter must apply the same rule the constructor applies.
Look at the last part of main. p.set_age(151) is refused, and the age is still 20. The setter rejected the change, so the stored value stayed valid. If set_age had no check, the call would store 151, and the object would break its own rule with no error at all.
Notice where the rule lives. The age range appears only in set_age. If you need to change the range from 150 to 120, you change one line. If the same check were copied into five places, one copy would eventually be missed.
A common habit that helps here is to write the check as its own function, so every setter calls the same code. The next example shows that habit with a rule that depends on two fields.
Step three: rules that depend on two fields
Some rules link two fields. A hotel stay has a check-in day and a check-out day. Neither number is wrong on its own, but the pair must be valid together. The check-out day must be after the check-in day, and the stay must last no more than 30 nights.
This creates a problem for setters. If you have set_check_in and set_check_out, each setter only sees one new value. The check must use the value the other field already holds. The setter for check-in must compare the new check-in day with the current check-out day, and the setter for check-out must compare the new check-out day with the current check-in day.
The safe way is to put the whole rule in one function that takes both values, and to call it from the constructor and from both setters.
Expected output:
nights: 4
rejected: check-out must be after check-in
still 3 to 7
now 3 to 20, nights: 17
rejected: a stay can be at most 30 nightsWalk through the first rejected call. The stay is from day 3 to day 7. set_check_in(9) asks for check-in on day 9. The setter calls validate(9, 7). The check out <= in is 7 <= 9, which is true, so the function throws. The check-in day stays at 3.
The next change, set_check_out(20), calls validate(3, 20). Every check passes, so the value is stored. The stay is now 17 nights.
The last call, set_check_out(60), calls validate(3, 60). The stay would be 57 nights, which breaks the 30-night rule, so it is refused.
The order of the checks matters for the message. The first check catches a day before the start of the hotel's calendar. The second catches a stay that ends before it starts. The third catches a stay that is too long. Each message names one broken rule, so the caller knows what to fix.
Why the check comes before the change
Look at the setter's order again: validate(...) first, then check_in_ = day. This order is what keeps the object valid when a check fails. If the assignment came first, the field would hold the bad value while the exception was thrown, and the object would be broken. A good rule for every setter is to validate the new values, and only then change the fields.
This order also matters for the caller. A caller who catches the exception still holds a valid object, with the old values, and can try again with a different value. An object that was half-changed when the exception was thrown would leave the caller with no safe value to fall back on.
Step four: keep the fields private
Every check above is worth nothing if a caller can write to a field directly. Suppose age_ were public. Then p.age_ = -5; would compile, and the setter would never run.
Here is what the compiler says when a class keeps its field private and some code tries to write to it.
inv_err_private.cpp: In function ‘int main()’:
inv_err_private.cpp:15:7: error: ‘int Person::age_’ is private within this context
15 | p.age_ = -5;
| ^~~~
inv_err_private.cpp:10:9: note: declared private here
10 | int age_;
| ^~~~This error is the protection working. The field can be changed only by code inside the class, and that code is the code that checks the rule. The rest of the program has to go through set_age, which refuses the bad value.
A small struct with public fields is fine for plain data that has no rules. A class with an invariant should keep its data private.
Example three: operations that change state
So far the changes have come through setters, which replace one field with a new value. Many classes change their state in other ways. A bank account has deposit and withdraw. These methods do not take a new balance. They take an amount and work out the new balance themselves. A method that changes an object's state is subject to the same rule as a setter, and it must keep every invariant.
The account below has one invariant: the balance is never negative. It also has a rule for the owner's name, which is checked in the constructor. The money is stored in cents, the smallest unit, as a long long, which is a whole-number type large enough for big totals. Storing money in whole cents avoids the rounding errors that come with floating-point numbers.
Expected output:
balance: 10000 cents
after deposit: 12550 cents
rejected: insufficient funds
rejected: withdrawal must be positive
rejected: owner must not be empty
final: 10000 centswithdraw shows the pattern for an operation. It checks the amount is positive, then checks the balance can cover it, and only then changes balance_cents_. Each check guards the rule that the balance never goes below zero. The second check reads the current balance, so the rule is enforced against the actual state of the object at the moment of the call.
The rejected withdrawal of 20000 cents leaves the balance at 12550, because the check threw before the subtraction ran. The final withdrawal of 2550 cents brings the balance down to exactly 10000, which is the value the account started with. The rule still holds at the end, because 10000 is not negative.
The same design question appears in every class that holds a total or a count. A stock count should not go below zero. A seat count in a cinema should not exceed the number of seats. Whenever a method changes a field, ask which rule the field must keep, and write the check before the change.
Common mistakes
Checking in the constructor only
A class that checks only in its constructor breaks as soon as a setter is added without a check. The constructor's check protects the first value, and the setter's missing check lets a second, invalid value in later. The fix is the one shown above: put the rule in one function, and call that function from the constructor and from every setter.
Using assert for input checks
assert is a macro from the <cassert> header. It checks a condition and stops the program if the condition is false. It looks like a validation tool, but it is meant for finding bugs in the programmer's own code, not for refusing bad input from a user or a file.
The problem is that an assert is removed when the program is built with NDEBUG defined. Many release builds define NDEBUG, a macro that switches assert off, so the check disappears. Here is a program that uses assert for the age rule.
Built normally with g++ -std=c++20 -Wall, the program stops at the assert. The output names the program and the source line where the check failed. Here the source file was saved as person.cpp:
person: person.cpp:7: Person::Person(int): Assertion `age >= 0 && age <= 150' failed.Built with g++ -std=c++20 -Wall -DNDEBUG, the same program prints a value that breaks the rule:
age stored: -5The second run is the dangerous one. Nothing stopped the bad value, and nothing reported it. For a rule that protects real data, use the throw pattern from this lesson, because it runs in every build.
Checking the value but not the state
A setter often needs to check the new value against the current state of the object. The reservation example shows this. A setter for check-out that checks only that the number is positive lets through a check-out day before the check-in day. Always ask what the other fields hold, and check against them.
Practice
Exercise 1. Write a set_celsius setter for a Temperature class. The rule is that a temperature cannot be below absolute zero, which is -273.15 degrees Celsius. Throw std::invalid_argument with a clear message when the value is too low.
Show the answer
void set_celsius(double value) {
if (value < -273.15) {
throw std::invalid_argument("temperature cannot be below absolute zero");
}
celsius_ = value;
}The check runs first, and the field changes only when the check passes. The constructor should call set_celsius so the rule lives in one place.
Exercise 2. The class Reservation has a setter set_check_out. A programmer changes it to check only that the new day is greater than zero. Explain what breaks.
Show the answer
A check-out day can now be earlier than the check-in day. For example, a stay from day 10 to day 4 would pass the new check and break the first rule of the class. The check must compare the new check-out day with the current check-in day, which is why the original code calls validate(check_in_, day).
Exercise 3. Add a method void birthday() to Person that adds one to the age. Decide how it should follow the rule that the age stays between 0 and 150.
Show the answer
void birthday() { set_age(age_ + 1); }The method reuses set_age, so the rule is checked in one place. When the age is already 150, the call throws, and the age stays at 150. This was tested: a Person at 149 becomes 150 after one birthday, and the next call is rejected with the message "age must be between 0 and 150".
Exercise 4. A class has a constructor that checks a rule and a setter that does not. Describe how a caller can break the rule, and say what the caller sees when it happens.
Show the answer
The caller builds a valid object, then calls the setter with a bad value. The setter stores the value without a check. The caller sees no error. The object now holds a value that breaks its rule, and the bug appears later, in code that reads that value. The fix is to add the same check to the setter, or to move the rule into one shared function.
Exercise 5. Predict the output of this program, then explain why the second line of output is still 20.
Person p{"Kiran", 20};
try {
p.set_age(151);
} catch (const std::invalid_argument& error) {
std::cout << "setter rejected: " << error.what() << '\n';
}
std::cout << "still " << p.age() << '\n';Show the answer
The output is setter rejected: age must be between 0 and 150 followed by still 20. The setter throws before it assigns age_, so the stored age never changes. The catch block prints the message, and the still line prints the unchanged value.
Summary
- An invariant is a rule that must always hold for an object, such as an age between 0 and 150.
- Check the rule in the constructor, in every setter, and keep the fields private so no other code can skip the checks.
- Validate new values first, and change the fields only after the checks pass.
- For a rule that links two fields, put the check in one shared function and call it from the constructor and from each setter.
- Do not rely on
assertfor input checks, because a build withNDEBUGremoves it.