C++ Constructors and Encapsulation
Establish valid objects with constructors and initializer lists, protect invariants behind focused methods, and avoid partially initialized state.
Before this lesson
Establish invariants in a constructor
Use member initializer lists correctly
Expose behavior instead of unrestricted setters
The short answer
A constructor establishes an object's valid starting state before callers can use it. Initialize members in the initializer list, keep invariant-sensitive data private, and expose operations that preserve the rules.
Validity begins during construction
An object should not escape in a half-configured state. A constructor receives required data, validates the contract, and either establishes a valid object or reports failure. This removes the period between default construction and a series of setter calls during which the object violates its own rules.
Require only what truly belongs to the invariant. Optional state can use a meaningful default or an optional representation rather than placeholder strings and magic numbers.
#include <iostream>
#include <stdexcept>
class Temperature
{
public:
explicit Temperature(double celsius) : celsius_{celsius}
{
if (celsius_ < -273.15)
throw std::invalid_argument{"Below absolute zero"};
}
double celsius() const { return celsius_; }
private:
double celsius_;
};
int main()
{
Temperature room{22.5};
std::cout << room.celsius() << '\n';
}Initializer lists construct members directly
Members are constructed before the constructor body runs. An initializer list supplies their construction arguments directly. Assigning inside the body first default-constructs a member and then replaces it, and it cannot initialize references or const members.
Members initialize in declaration order. Keep the initializer list in that same order to avoid misleading readers and warnings. Use std::move when a by-value parameter intentionally transfers its owned contents into a member.
Encapsulation protects transitions
Private data is useful when unrestricted assignment could violate an invariant. Expose operations named after domain behavior: deposit, try_withdraw, or rename_topic. Each operation validates its own transition and leaves the object valid on every return path.
| Interface | Problem | Stronger alternative |
|---|---|---|
| set_balance(any int) | Allows negative balance | deposit and try_withdraw |
| set_status(any value) | Allows impossible transition | approve or cancel |
| public vector member | Any caller can bypass rules | add_item and remove_item |
| read-only accessor | Observes without mutation | Keep when callers need the value |
Defaults and special members are design choices
The compiler can generate default construction, copying, moving, and destruction. Accept those operations only when they preserve the type's meaning. A value-like temperature can be copied; a unique resource owner should move but not copy; an entity may need a deliberate identity rule.
Prefer the Rule of Zero by composing standard types. Use = default to state an appropriate generated operation and = delete to reject an operation that would break the contract.
Quick knowledge check
Answer before you reveal.
01Why initialize a const member in the initializer list?
It must be constructed with its value; assignment inside the body is too late.
02Is a public setter always encapsulation?
No. A setter that accepts every value may expose the same invalid transitions as a public field.
Exercise
Practice challenge
Build a BankAccount class requiring a non-empty owner and non-negative opening balance. Add deposit and try_withdraw operations that preserve validity.
Requirements
- No usable account can start with invalid state
- Balance cannot be assigned directly by callers
- Expected withdrawal rejection does not corrupt state
Optional extension: Delete copying if account identity should not be duplicated.
Open in C++ compilerLesson checkpoint
One small step locks it in
Mark this lesson complete, then keep the momentum going.
Clear up the details
Frequently asked questions
In what order are members initialized?
In their declaration order inside the class, not the visual order of the initializer list.
What does explicit do on a constructor?
It prevents the constructor from being used for unintended implicit conversions in applicable single-argument cases.
Should constructors perform file or network work?
Usually keep construction focused and predictable. Use a factory or separate operation when creation can involve substantial recoverable I/O.