Class invariants, static members, and friends
Build classes that protect valid state, understand members shared by all objects, and use friend access only when it improves a design.
Learning objectives
- Define a class invariant.
- Keep data private and enforce valid state through constructors and operations.
- Distinguish per-object members from static class members.
- Explain what friend declarations allow and when to avoid them.
Prerequisites: classes, constructors, access control, and static members. Review Classes and objects, Class design, and Constructors and destructors first.
Class invariants
An invariant is a condition that should remain true for every valid object whenever public operations begin and finish. For a bank account, one possible invariant is that the balance must never be negative. Private data prevents callers from bypassing the class's rules.
#include <stdexcept>
class Account {
int balance;
public:
explicit Account(int opening_balance) : balance{opening_balance} {
if (opening_balance < 0) {
throw std::invalid_argument("opening balance cannot be negative");
}
}
int get_balance() const {
return balance;
}
bool withdraw(int amount) {
if (amount <= 0 || amount > balance) {
return false;
}
balance -= amount;
return true;
}
};The constructor rejects a negative opening balance instead of silently changing it to zero. The withdrawal operation checks its preconditions before changing the balance. Here, invalid or unaffordable withdrawals return false; another API could report invalid input with an exception. In a real financial application, monetary values and business rules require more careful types and error handling; this is a small class-design example.
Static data members
A non-static data member belongs to each object. A static data member belongs to the class and is shared by all its objects.
class Widget {
public:
inline static int default_constructor_calls{0};
Widget() {
++default_constructor_calls;
}
};This counter records calls to this default constructor. It is cumulative; it is not a count of currently alive Widget objects, and compiler-generated copy or move construction does not increment it. A live-object counter would need to be updated by every constructor and the destructor. inline static data members are available in C++17 and later. For earlier standards, declare the static member in the class and provide one out-of-class definition in a source file.
Static member functions
A static member function belongs to the class rather than to a particular object. It has no implicit this pointer, so it cannot directly access a particular object's non-static data members.
class Temperature {
double celsius;
public:
explicit Temperature(double c) : celsius{c} {}
static double to_fahrenheit(double c) {
return c * 9.0 / 5.0 + 32.0;
}
};Call it as Temperature::to_fahrenheit(20). It does not need a Temperature object because the calculation uses only its argument.
Friend declarations
A friend function or class is allowed to access private and protected members of the class that grants friendship. Friendship is explicit and does not automatically pass to derived classes or unrelated functions.
#include <iosfwd>
class Point {
int x;
int y;
public:
Point(int x_value, int y_value) : x{x_value}, y{y_value} {}
friend std::ostream& operator<<(std::ostream& out, const Point& point);
};The stream insertion operator can be defined outside the class and access the private coordinates. Include <ostream> in the source file that defines it. Do not use friendship as a shortcut for exposing every internal field; prefer a narrow public interface when possible.
Design checklist
- Keep representation details private.
- Establish invariants in constructors.
- Validate inputs before changing state.
- Use const member functions for observations.
- Use static members only for data or operations genuinely associated with the class as a whole.
- Grant friendship narrowly and for a clear reason.
Practice exercises
- What is a class invariant, and where should it be established?
- Does each object get its own copy of a static data member?
- Why can't a static member function directly use a non-static data member?
- What additional access does a friend function receive?
Solutions
Show exercise solutions
- A condition that must remain true for valid objects. Constructors establish it, and public operations preserve it.
- No. A static data member belongs to the class and is shared by all instances.
- It has no implicit object and therefore no
thispointer identifying which instance to access. - It may access private and protected members of the class that declares it as a friend.
Summary
Good classes protect their invariants. Static members belong to the class; ordinary members belong to objects. Friend declarations provide controlled access but should be used sparingly.