#112 – Enforcing encapsulation in C++

Introduction

Encapsulation is a technique used to enforce the separation of interface and implementation by hiding (making inaccessible) the implementation of a program-defined data type from users. We use access specifiers to achieve this objective.

Consider a classic OOP-based class

Since I’m hungry, let’s talk about a Pizza class:

#include <iostream>
#include <string>
#include <string_view>

class Pizza {
private:
    std::string m_flavour { "Margherita" };

public:
    void setFlavour(std::string_view flavour)
    {
        m_flavour = flavour;
    }

    const std::string& getFlavour() const
    {
        return m_flavour;
    }

    void print() const
    {
        std::cout << "Pizza flavour: " << m_flavour << '\n';
    }
};

int main()
{
    Pizza pizza {};

    pizza.setFlavour("Pepperoni");
    pizza.print();
}

// Terminal output:
Pizza flavour: Pepperoni

This works but notice how print() belongs in the class.

If the class is updated, print() must be updated since it interacts with the member variable. If we want to adjust print(), we need to change the class.

The issue is not that print() accesses m_flavour. Member functions are allowed to access private data. The issue is whether printing is truly part of the core responsibility of Pizza.

Second version: use the public interface internally

We enforce encapsulation by making print() use the public interface:

#include <iostream>
#include <string>
#include <string_view>

class Pizza {
private:
    std::string m_flavour { "Margherita" };

public:
    void setFlavour(std::string_view flavour)
    {
        m_flavour = flavour;
    }

    const std::string& getFlavour() const
    {
        return m_flavour;
    }
    
		// print() uses the public interface
    void print() const
    {
        std::cout << "Pizza flavour: " << getFlavour() << '\n';
    }
};

int main()
{
    Pizza pizza {};

    pizza.setFlavour("Hawaiian");
    pizza.print();
}

// Terminal output:
Pizza flavour: Hawaiian

However, print() is still a member function. The Pizza class is still responsible for formatting output.

That may be unnecessary.

Third version: move printing outside the class

If printing does not need private access, it can be a non-member function:

#include <iostream>
#include <string>
#include <string_view>

class Pizza {
private:
    std::string m_flavour { "Margherita" };

public:
    void setFlavour(std::string_view flavour){
        m_flavour = flavour;
    }

    const std::string& getFlavour() const{
        return m_flavour;
    }
};

void print(const Pizza& pizza){
    std::cout << "Pizza flavour: " << pizza.getFlavour() << '\n';
}

int main(){
    Pizza pizza {};

    pizza.setFlavour("Supreme");
    print(pizza);
}

Terminal output:
Pizza flavour: Supreme

This is the cleaner design.

The Pizza class now owns the data and exposes the public operations needed to interact with it:

setFlavour()
getFlavour()

The print() function lives outside the class because it does not need privileged access.

It uses the public interface:

pizza.getFlavour()

That means printing can change without changing the class itself.

Conclusion

Encapsulation is more than private. It’s about determining the responsibilities of the class and designing the public interface. The final version of the Pizza example conveyed clear responsibilities to each object:

  1. Pizza manages pizza state.
  2. print() formats a pizza for output.