How to create an immutable class in C++

Let’s say you want to create an immutable object for the various benefits that they offer like thread safety, improved readability and higher safety than mutable objects. C++ offers two ways of restricting mutation: the const and the constexpr (since C++11) keywords.

In this post, let’s explore the idea behind using const to create immutable objects.

const member variables

The simplest way to create an immutable object is to create classes in which all members are constant.

class Employee
{
public:
   int const         m_id;
   std::string const m_Name;

   ...
}

With the above class, there is no need to define accessor functions for the member variables. They have public access and they are shielded against any modifications because they are declared const. But this has an undesirable side-effect – the copy assignment and the move assignment operators are deleted.

For example, if you try to assign an Employee object e2 to another Employee object e1, you would get the following compilation error

error: object of type 'Employee' cannot be assigned because its copy assignment operator is implicitly deleted
    e1 = e2;
       ^
note: copy assignment operator of 'Employee' is implicitly deleted because field 'm_Id' is of const-qualified type 'const int'
    int const m_Id;
              ^
1 error generated.

This is expected behavior because variables that are defined const can only be initialized during construction.

const member functions

A different approach would be trying to make all the public member functions constant. First, let’s see how const relates to member functions. Consider a class Employee with two member functions that return the Id and Name of the employee.

class Employee
{
private:
   int         m_Id;
   std::string m_Name;

public:
   Employee(int const id, std::string const &name) : m_Id(id), m_Name(name) {}

   int get_id() const;

   std::string get_name() const;
};

We know that member functions of a class have an additional implicit argument called this. The member functions get_id() const and get_name() const internally look something like this:

int Employee_get_id(Employee const * const this);

std::string Employee_get_name(Employee const *const this);

this is a constant pointer to an instance of the object the member function belongs. Making the member function const will make this point to a constant object whose members cannot be changed.

The const qualifier on a member function is nothing more than a qualifier for the type that this is pointing to. By using const, you’re telling the compiler you don’t want a variable to be mutable, and the compiler will give you an error message any time you try to change that variable.

By making the member functions const, the user of the class is not allowed to modify the member variables because this points to an instance of const Employee in all the member functions. This provides both logical const-ness (the user-visible data in the object never changes) and internal const-ness (no changes to the internal data of the object). This approach is also desirable because the compiler will generate all the necessary move operations for you.

use of mutable to allow for internal changes

The only other use case that does not fit the constant member variable solution is when you need to be able to change your internal data but hide those changes from the user (being immutable to the user).

This is especially needed, for example, when you want to cache the result of a time-consuming operation(using memoization). Consider a new member function of the Employee class which returns the employment history of an employee. It is implemented as follows

class Employee
{
private:
   int                   m_Id;
   std::string           m_Name;
   employment_history_t  m_Employment_History;

public:
   employment_history_t get_employment_history() const
   {
      return load_employment_history_from_db(m_Id);
   }
};

As you can see, the load_employment_history_from_db(m_Id) is called everytime we need to get the employment history of the employee. This is also an expensive database read operation. This might be unnecessary for all object, since the employment history for only a few types of employees is really needed.

This can be achieved by creating a mutable member variable – a member variable that can be changed even from const member functions. This makes sure that the class is still immutable from the user’s perspective but can modify the mutable variables internally.

class Employee
{
private:
   int                           m_Id;
   std::string                   m_Name;
   mutable employment_history_t  m_Employment_History;

public:
   Employee(int const id, std::string const &name) : 
      m_Id(id),
      m_Name(name) {}

   employment_history_t get_employment_history() const
   {
      if (!m_Employment_History.loaded())
      {
         load_employment_history(m_Employment_History);
      }

      return m_Employment_History;
   }

This implementation keeps the class immutable on the outside but allows internal data to change sometimes.