Tutorials Logic, IN info@tutorialslogic.com

C++ Encapsulation and Class Invariants

Invariant Protection

Encapsulation keeps an object’s valid state behind a small public interface. Private data alone is not enough: constructors and mutating methods must enforce the same invariant, and no returned reference or pointer should let callers bypass those checks.

Guard the Invariant at the Boundary

The account never allows a negative balance because callers cannot assign the field directly and withdraw validates before mutation.

Encapsulate a balance

Encapsulate a balance
#include <iostream>
#include <stdexcept>

class Account {
public:
    explicit Account(int opening) : balance_{opening} {
        if (opening < 0) throw std::invalid_argument{"negative opening balance"};
    }
    void deposit(int amount) { if (amount <= 0) throw std::invalid_argument{"deposit"}; balance_ += amount; }
    bool withdraw(int amount) { if (amount <= 0 || amount > balance_) return false; balance_ -= amount; return true; }
    int balance() const { return balance_; }
private:
    int balance_;
};

int main() { Account a{100}; a.withdraw(30); std::cout << a.balance() << '\n'; }
Output
70

Return Values Without Leaking Mutation

Returning a const reference can avoid a copy but ties callers to object lifetime and may expose internal storage. Return small values by value and expose views only when lifetime rules are clear.

  • Keep invariants in one class.
  • Name methods after domain operations.
  • Use const member functions for observations.

Stable Class Interface

Expose operations that name user intent rather than a sequence of setters. An Account::withdraw operation can check amount and balance together, while separate public balance mutations cannot preserve that rule. Keep representation details private so storage can change without rewriting callers.

Const member functions promise not to modify observable object state and make read-only use possible. Return values for small data and carefully bounded const references for owned data whose lifetime is clear. Avoid friend access unless two types genuinely implement one tightly coupled abstraction.

Preserve Invariants at Every Mutation

Encapsulation is useful when the class can guarantee a rule that callers cannot bypass. Keep representation private, validate constructor input, and expose operations named after domain behavior. Returning a mutable reference to private storage defeats the boundary even when the field itself is private.

A mutating method should either establish the complete next valid state or leave the object unchanged. Return a result that lets the caller distinguish rejection from success without exposing internal fields.

Reject a Withdrawal Without Corrupting State

Reject a Withdrawal Without Corrupting State
#include <iostream>

class Account {
    int balance_;
public:
    explicit Account(int opening) : balance_(opening >= 0 ? opening : 0) {}
    bool withdraw(int amount) {
        if (amount <= 0 || amount > balance_) return false;
        balance_ -= amount;
        return true;
    }
    int balance() const { return balance_; }
};

int main() {
    Account account(50);
    std::cout << std::boolalpha << account.withdraw(70) << '\n';
    std::cout << account.balance() << '\n';
}
Output
false
50

The rejected operation leaves the balance invariant intact, and callers can observe only the safe read interface.

Before you move on

Invariant Review

5 checks
  • Construction establishes valid state.
  • Mutations preserve the invariant.
  • No setter bypasses domain rules.
  • Observers do not expose unsafe mutation.
  • Failure behavior is testable.

Try this next

Encapsulation Practice

0 of 2 completed

  1. Build an Account class that rejects negative withdrawals and never exposes a mutable balance reference. Test the state before and after a rejected operation.
  2. Start with a class whose fields are public, identify the operations callers actually need, and replace direct writes with those methods. Keep validation at the mutation boundary.

Encapsulation Questions

No. Expose operations the object can safely perform instead of recreating public fields through accessors.

Validate them in constructors and public mutating methods so invalid state cannot enter through normal use.

It promises not to modify the object and allows the getter to be called on const instances.

Next Step
Next Practice

Finish the concept here, then reinforce it with hands-on coding, interview prep, or a tool that matches the topic.

Browse Free Tutorials

Explore 500+ free tutorials across 20+ languages and frameworks.