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.