A close-up of a heavy metal door with a steel lock. Photo · K Adams / Unsplash
Inside: a thousand moving parts. Outside: one door, one handle. That is the whole point of encapsulation.

Last week your Point exposed its x and y directly. Anyone could write p.x = -999 from anywhere in the program. That's fine for a Point — coordinates can be anything. But for almost every other class you'll ever write, letting outside code touch your fields directly is a recipe for the program slowly losing its mind.

The fix is the simplest, oldest, most quietly important rule of object-oriented design: hide your data behind methods. Mark fields private. Provide public methods that read or modify them, and that uphold the rules of the type as they do so. The class becomes a black box: a small, knowable, unchanging public interface — and behind it, whatever internal mess you need.

Three access levels

Why bother?

A small example. Suppose we model a temperature, in Celsius, with the constraint "a temperature must always be at or above absolute zero, −273.15 °C."

class Temperature {
public:
    Temperature(double celsius) {
        set(celsius);     // reuse the validation
    }
    void set(double celsius) {
        if (celsius < -273.15) throw std::invalid_argument("too cold");
        celsius_ = celsius;
    }
    double celsius()    const { return celsius_; }
    double fahrenheit() const { return celsius_ * 9.0 / 5.0 + 32; }
    double kelvin()     const { return celsius_ + 273.15; }

private:
    double celsius_;
};

// caller side:
Temperature t(25);          // 25 °C — valid
t.set(-300);             // throws — caught and rejected
t.celsius_ = -300;          // COMPILE ERROR: celsius_ is private

Notice what's gained: the rule "a temperature is never below absolute zero" is enforced by the language. Outside code cannot bypass it, no matter how careless or deliberate. The only way to set the value is through set(), which validates. The compiler refuses every other path. The black box is sealed.

The trailing-underscore convention

In the example I named the private field celsius_ with a trailing underscore. This is one of the most common conventions in C++ codebases (Google's style guide uses it; LLVM uses m_ as a leading prefix; some teams use neither). The point is to make it visually obvious which names are private member fields as opposed to local variables or parameters.

This matters because constructors and setters often want to take a parameter with the same conceptual name as the field — celsius the parameter, celsius_ the field. Without a naming convention, you'd be writing this->celsius = celsius everywhere. With one, you write celsius_ = celsius and the compiler doesn't get confused.

The interface vs the implementation

The single most important benefit of encapsulation is that it lets you change the implementation without breaking callers.

Suppose three years later, for performance reasons, you decide to store temperatures internally as kelvin instead of celsius:

class Temperature {
public:
    // public interface unchanged — same methods, same names, same meaning
    Temperature(double celsius)           { set(celsius); }
    void   set(double celsius)            { /* validate, store as kelvin */ }
    double celsius()    const            { return kelvin_ - 273.15; }
    double fahrenheit() const            { return celsius() * 9.0 / 5.0 + 32; }
    double kelvin()     const            { return kelvin_; }

private:
    double kelvin_;     // changed!
};

Every caller on Earth still works without modification. t.celsius() still returns the right number; the conversion just runs each time it's called. You changed the storage format and the entire rest of your codebase didn't have to know.

This is the dividend that encapsulation pays: the freedom to refactor. In a world where the field celsius_ was public, every caller would now be referring to a field that doesn't exist, and you'd be staring at five hundred compiler errors.

Getters and setters — necessary, often overdone

The pattern "private field, public getter, public setter" is sometimes called property-style access. It's good when the getter/setter does real work — validation, lazy computation, derived values, change notifications. It's bad when it's just dressed-up public access:

// pointless — exactly equivalent to a public field, plus 4 lines of noise
class Foo {
public:
    int x() const      { return x_; }
    void set_x(int v)   { x_ = v; }
private:
    int x_;
};

If the field is genuinely just a value with no rules, just make it public. The Plain-Old-Data struct is a perfectly respectable design. Encapsulation isn't a tax on every member; it's a tool for the cases where you want to enforce something. Use it where you mean it.

Friend — when you really need to peek

Sometimes a class genuinely needs another class (or a free function) to see its privates. C++ has the friend keyword for exactly this:

class Vector {
public:
    Vector(double x, double y): x_(x), y_(y) {}
    friend std::ostream& operator<<(std::ostream&, const Vector&);
private:
    double x_, y_;
};

std::ostream& operator<<(std::ostream& os, const Vector& v) {
    return os << "(" << v.x_ << ", " << v.y_ << ")";
}

The friend declaration says "this free function, although not a member of Vector, is allowed to see private fields". This is the right tool for things like printing operators, swap functions, and tightly-coupled helper classes. Use it sparingly. It's a deliberate hole in the wall — make sure you really need it.

Why this matters for AI

The at::Tensor class in PyTorch has dozens of public methods (.size(), .dtype(), .contiguous()) and a handful of carefully-controlled private internals (the storage pointer, the strides table, the device handle). The internals have changed several times across PyTorch versions — the public API hasn't. Every Python user on Earth has been protected from those internal changes by a single line of private: in C++.

Same story for nearly every long-lived AI library. The discipline of "keep the front door narrow; rearrange the kitchen as needed" is exactly what lets a piece of software live for ten years.

Encapsulation isn't paranoia. It's a permission slip from your future self to refactor freely.

Try it yourself

What's next

Sometimes you want a class that's like another class but slightly different — extends it, adds new fields, overrides some methods. C++ has a built-in way to express that relationship.

Week 21 is The Family Tree — inheritance, base classes, and derived classes.

Photo credit

Photo free under the Unsplash license. Vault · K Adams.