A C# Programmers Understanding of Const Correctness in C++

On my mission to learn modern C++ I came across the concept of const correctness which is a term describing the application of the const keyword to class methods. Since C# has no concept of constant methods, and it confused the heck out of me, I wanted to share what I learnt.

The purpose of const

Let's first start with a quick introduction to the idea of constant and its purpose in programming.

Like in C#, there is data in our code that we don't want to change. A common example would be to represent constant values you find in mathematics like PI or the natural number e. In a program we would store these values in a variable, but we wouldn't want the values of those variables to ever change in our program.

We could simply write a comment telling the reader of our code to not change the values but that doesn't guarantee it won't happen. Instead, we can use the const keyword which allows the compiler to enforce our intentions that the values are immutable. If someone tries to change the values of a const variable, your IDE will likely warn you about this action, and if you try to compile your program it will fail.

    // use keyword to stop the variables from being changed
    const float PI = 3.141;
    const float e =  2.718;
    
    // error cannot assign to const variable
    PI = 2;

Understanding const and class methods

Unlike C#, C++ also allows us to set class methods to be const. The purpose is to specify methods in a class that doesn't alter the state of the object, which also enables them to be callable from an object marked as const.

Similar to what we looked at above with variables, if we attempt to modify a member variable inside of a const method, the compiler will complain and not compile the program.

class A
{
public:
    int GetVal() const {return _val;}
    void SetVal(int val) { _val = val;}
private:
    int _val = 1;
};

const A a1; // a1 is const so can only call GetVal
A a2; // a2 is non-const so can call both GetVal and SetVal

In the above example, class A has two methods that either set or get the value of the variable _val. The getter method GetVal is marked as const since it doesn't modify _val so can be used on const objects of type A. The setter method SetVal alters the value of _val and therefore won't be marked as const.

If we didn't mark GetVal as const we couldn't access that method on an const object of type A but we want to be able to do that since that method doesn't modify the object.

"Breaking" const methods with mutable

Although const methods seem easy on the surface C++ adds its usual complexity by adding the idea of mutability to the mix. The language allows you to apply mutable to a variable in a class which allows that variable to be altered inside of a const method.

class A
{
public:
    int GetVal() const {return _val;}
    void SetVal(int val) const { _val = val;}
private:
    mutable int _val = 1;
};

const A a1; // a1 is const and we marked SetVal to const and _val to be mutable so that SetVal can be called and change the value of _val despite it being const!

In the above code we take the previous example and mark SetVal as const and _val as mutable. Now we are able to alter a1 even though it's meant to be a const object!

At first glance the idea that we can bypass the guarantees that const methods provide with mutable seems wrong. However, after I came across this article on the ISOCPP wiki, I came to understand the purpose of having this feature.

Understanding an object's physical and logical state

The article I linked above it introduces the concept of an object having two states, a physical state and a logical state.

The physical state refers to the internal state of an object or in other words the data stored in the variables defined in the object's class. In our example of class A its physical state is _val and the value that variable holds for a1.

The logical state is the external state of the class, referring to the public methods or variables that make up the interface for the class. In A its logical state refers to the methods GetVal and SetVal. If we made _val public as well then it would also be regarded as part of its logical state.

Since SetVal in our original example is not const we know that it will change the logical state of the class, and that GetVal, being const doesn't.

Const correctness only applies to logical state

The primary reason for needing to understand these two states is that const correctness should only be applied to the logical state and not the physical state.

Typically, when we incorporate good class design principles, the inner workings of our class are hidden from the user. These implementation details refer to things like the choice of data structure, use of caching, retrieving information from a database etc. These things can represent the physical state of an object, and should not be visible to the user.

For example, a user shouldn't typically care if retrieving information about a player in a game comes from a file on disk, a database, a local cache or some other location.

Based on this logic it doesn't make sense to base the decision to make a method const around the physical state. Especially, when the purpose of const is to provide intent to developers using the code.

The choice to choose a logical state over a physical state becomes even more apparent when we see the physical state of an object can differ from its logical state.

Example of when logical and physical states differ

class Multiplier
{
public:
     Multiplier(int a, int b) : _a(a), _b(b) {}

    int Result() const { return _a * _b; }
    
private:
    int _a;
    int _b;
};

In the above example we have a class Multiplier that takes two numbers a and b and returns the product of these numbers by calling Result. The physical state of the object is the variables _a and _b which differs from its logical state, the product of _a * _b.

Whenever we call Result we will get the same value back _a * _b which is why it's safe to mark the value as const. In this example our code also doesn't alter the physical state.

The logical state is separate from the physical state in this example since it doesn't store the result of the value in the class.

Example of when logical state changes but not the physical state

class RandMultiplier
{
public:
        Multiplier(int a, int b, int lowerLimit, int upperLimit) : _a(a), _b(b), _lowerLimt(lowerLimit), _upperLimit(upperLimit) {}

    int Result() { return _a * _b * rand() % _upperLimit + _lowerLimit; }
    
private:
    int _a;
    int _b;
    int _lowerLimit;
    int _upperLimit;
};

In the above example we create a new class RandomMultiplier which applies a random value to the product of _a and _b. In this example both the logical and physical states are different, similar to the previous example.

More importantly the logical state is no longer regarded as constant since each time we call Result we will get a different value. If we were marking methods const based purely on their ability to alter physical state we would have marked Result as const, which would be confusing since it returns a different value each time.

How does mutable fit in

In the previous examples we have described logical state that is both constant and non-constant. In neither situation was the physical state of the object changed. The problem mutable solves is when we want to alter the physical state of an object but we have said that the logical state is constant.

This can happen for a number of reasons, for example using a "flag" variable to stop an expensive calculation from repeatedly being called. Alternatively, you could use it to lock a mutex when running multi-threaded code to avoid issues with multiple threads accessing data at the same time.

In either situation we wouldn't be able to modify an objects member variable inside of a const method without the ability to mark it as mutable.

class Multiplier
{
public:
     Multiplier(int a, int b) : _a(a), _b(b) {}

    int Result() const 
    { 
        if(!_calculatedResult)
        {
            _result = _a * _b;
            _calculatedResult = true;
        }
        return _result;
      }
    
private:
    int _a;
    int _b;
    mutable int _result = 0;
    mutable bool _calculatedResult = false;
};

In the above code we change the Multiplier class slightly. Instead of always returning the product of _a and _b, we calculate the result once, cache it, and then return that result on all future calls to the method Result.

Since Result is marked as const since it doesn't change the logical state of the object we have to mark _calculatedResult and _result as mutable.

Obviously, in this example, it doesn't make sense to cache the result as multiplying two integers isn't expensive. However, for an expensive calculation this could be a suitable optimisation.

The point is that the logical state of the object remains constant even though the physical state has changed. And in order to change the physical state of an object in a const method we have to mark the required member variables as mutable.

Summary

In C++ const correctness refers to applying const to methods of a class to ensure that const objects cannot be mutated. In order to apply const correctness correctly in your program you must apply the const keywords to methods that do not alter the logical state of your object. If a const method needs to alter the physical state of the object you can assign the mutable keyword to any member variables that are used within the const method.