Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 6 of 12

Three labels control who may touch a member:

| | Reachable from | |---|---| | public | anywhere | | private | the class's own members and friends | | protected | the class, its friends, and derived classes (Module 16) |

#include <iostream>

class Example {
public:
    int open = 1;

private:
    int closed = 2;

public:
    int reveal() const { return closed; }    // the class can see its own privates
};

int main() {
    Example e;
    std::cout << e.open << "\n";
    // std::cout << e.closed;                // error: 'int Example::closed' is private
    std::cout << e.reveal() << "\n";
    return 0;
}
1
2

Labels apply to everything after them until the next label, and you can use each as many times as you like.

What encapsulation is actually for

The usual explanation — "hiding your data" — is misleading. Nothing is secret: anyone can read the header. The real purpose is narrower and more useful:

An invariant is a statement that is always true of a valid object. Encapsulation means every path that could break the invariant goes through code you wrote.

Without it, an invariant is a hope:

#include <iostream>

// No invariant can hold: anyone may set anything.
struct OpenRange {
    int low;
    int high;
};

int main() {
    OpenRange r{1, 10};
    std::cout << "size: " << (r.high - r.low) << "\n";

    r.high = -5;                    // nothing prevents this
    std::cout << "size: " << (r.high - r.low) << "\n";
    return 0;
}
size: 9
size: -6

A range with negative size is meaningless, and every function taking an OpenRange now has to defend against it. The check has to be repeated everywhere, forever.

With the members private, the rule lives in one place:

#include <iostream>

class Range {
    int low;
    int high;

    // The invariant: low <= high, always.
    void repair() {
        if (high < low) {
            high = low;
        }
    }

public:
    Range(int l, int h) : low(l), high(h) { repair(); }

    void set_high(int h) { high = h; repair(); }
    void set_low(int l) { low = l; repair(); }

    int size() const { return high - low; }    // can never be negative
};

int main() {
    Range r(1, 10);
    std::cout << "size: " << r.size() << "\n";

    r.set_high(-5);                            // repaired, not rejected
    std::cout << "size: " << r.size() << "\n";
    return 0;
}
size: 9
size: 0

size() needs no check, because there is no way to construct a Range whose size is negative. That is the payoff: the guarantee moves from every caller into one class.

Note repair() is itself private. Private members are not only data.

The freedom to change your mind

The second benefit is practical. As long as the public interface is stable, the internals are yours:

#include <iostream>

// Version 1 stores the total. Version 2 could store a list and sum it on
// demand, and no caller would need to change, because `add` and `total` are
// the whole contract.
class Ledger {
    double running_total = 0.0;
    int entries = 0;

public:
    void add(double amount) {
        running_total += amount;
        ++entries;
    }

    double total() const { return running_total; }
    double average() const { return entries == 0 ? 0.0 : running_total / entries; }
};

int main() {
    Ledger l;
    l.add(10.0);
    l.add(20.0);
    l.add(30.0);
    std::cout << l.total() << " " << l.average() << "\n";
    return 0;
}
60 20

Make running_total public and it becomes part of the contract by accident: someone will write to it, and you can never restructure it again.

Getters and setters are not automatic

A common misreading of encapsulation is to make every field private and add a getter and setter for each:

class Pointless {
    int x;

public:
    int get_x() const { return x; }
    void set_x(int v) { x = v; }      // enforces nothing
};

That has all the ceremony of encapsulation and none of the benefit — x is still arbitrarily settable, just through two extra function calls. If a setter enforces nothing, either the field should be public (make it a struct) or you have not yet worked out what the invariant is.

Ask what would make the object invalid. If nothing can, you have plain data.

friend

friend grants one function or class access to your privates:

#include <iostream>

class Money {
    long cents;

public:
    Money(long c) : cents(c) {}

    // Needs both objects' internals, so it cannot be a member of either.
    friend bool operator<(const Money& a, const Money& b);
    friend std::ostream& operator<<(std::ostream& os, const Money& m);
};

bool operator<(const Money& a, const Money& b) {
    return a.cents < b.cents;
}

std::ostream& operator<<(std::ostream& os, const Money& m) {
    return os << (m.cents / 100) << "." << (m.cents % 100);
}

int main() {
    Money a(1250);
    Money b(999);

    std::cout << a << " < " << b << " : " << (a < b) << "\n";
    std::cout << b << " < " << a << " : " << (b < a) << "\n";
    return 0;
}
12.50 < 9.99 : 0
9.99 < 12.50 : 1

friend is granted by the class, so it cannot be taken from outside. Use it sparingly — operator overloads that need two objects' internals are the classic legitimate case.

Try it

Write a class Stack over a fixed array of 10 ints with push, pop, top, empty and full. The invariant is that the item count is always between 0 and 10. Show that no sequence of public calls can break it.

Checkpoint

  • public, private, protected control reachability; private is the class default.
  • Encapsulation exists to protect invariants, not to keep data secret.
  • With private state, the rule is enforced in one place instead of at every call site.
  • A stable public interface leaves you free to change the representation later.
  • A setter that enforces nothing means the field should be public.
  • friend grants access; it is granted by the class and cannot be taken.

Questions about this lesson

Log in to ask a question.