It is very important to understand the details of const qualifiers to help improve the quality of your code and the efficiency of your coding

The C++ const
qualifier is one of the first things you learn about C++. You usually start by understanding that we should use const
for constants. That may be true, but actually we should think of it as a way to tell compilers and other programmers our intentions about our code.
When we add const
to a variable (there are other use cases discussed later), we are actually saying that we want this variable to be read-only or immutable. Any attempt to modify it should be stopped by a compile error.
In the next sections, I’ll cover different scenarios where we can use the const
qualifier in our C++ code and what actually happens under the hood.
const variables
The first thing that comes to mind, the easiest example is const
variables. This could be local variables in a function, member variables in a class, or global variables.
The purpose of adding a const
qualifier is to explicitly say that our intent is for the variable to be read-only. For example, take a look at the function below:
This function indicates that the author wants the val
parameter and the local variable two
to be read-only. Nothing special happens here. Both variables are stored on stack memory just like non-const variables. The only difference is, when we try to change the value, we will get a compile error. The following code shows an attempt to modify val
.
error: assignment of read-only parameter ‘val’
The same goes for const
member variables in classes, the only difference with non-const member variables is that when we try to modify them we will get the same compile error shown above.
For global variables, there is another difference, when we add a const
qualifier to the variable, the variable will be stored in the read-only section of memory instead of the data section. The rest is the same.
So it’s clear that the const
keyword is a way for us to communicate our intentions to compilers and other programmers to say that we don’t want variables to change at runtime.
Functions returning const
I find that this is rare, but possible. We can add a const
qualifier to the return type of a function. This only makes sense when we return a user-defined type, not a primitive type like an int
. Take a look at the example below:
Notice that CreateT()
returns a temporary variable that can be used to call SetVal()
. Changing the return value to be const
can prevent this from happening.
error: passing ‘const T’ as ‘this’ argument discards qualifiers [-fpermissive]
The compiler throws this error because we are trying to pass a pointer to a const
object to SetVal()
which only accepts non-const. In case you didn’t know or forgot, calling a member function means implicitly passing a pointer (this pointer) to the function. In this case,
int SetVal(T* this, const int val);
Passing const T*
to this function causes a compile error because we are trying to implicitly remove the const
qualifier.
However, there is a downside of returning a const
object, it prevents move semantics which was introduced from C++11. Now, let’s assume that our class T
allocates memory dynamically to manage some of its member variables. Of course, we want to use move semantics to avoid the expensive copying process.
If CreateT()
returns a const
object, line 3
in the code above will be invoking copy assignment operator, whereas if it returns a non-const object it will be invoking move assignment operator. This is because the compiler will choose the copy assignment operator overload which accepts a const
object reference.
So unless you’re using an older version of C++ there’s no reason to return a const
object. It would be good practice for the calling side not to call a function that tries to modify a temporary object.
const (member) functions
Another use of const
qualifier is for member functions of a class. The syntax is unique or awkward because it is placed at the end of a function declaration.
It should be written that way because the variable that it qualifies is hidden. Recall that all non-static member functions have a hidden pointer to themselves called this. The const
qualifier we are discussing here is for qualifying that pointer.
Going back to our example above, the T
class has a member function called SetVal()
.
int SetVal(const int val);
When you compile the code, the compiler modifies it to:
int SetVal(T* this, const int val);
To make the first variable const
we need to write our code as:
int SetVal(const int val) const;
So, the compiler modifies it to:
int SetVal(const T* this, const int val);
For now, ignore the fact that SetVal()
doesn’t make sense to be const
since we want to modify the object. The point I’m trying to make here is the const
qualifier that exists to make this pointer points to a const
object.
Any attempt to change any member variable in that function will cause a compile error. Another important point is the one that we discussed above, when you declare a const
object of type T
you can only call const
functions. That’s because you can’t convert a pointer to a const
object to a pointer to a non-const object because it would violate constness.
Converting const to non-const and vice versa
There is something that I find confusing especially for beginners. That is, passing a parameter by reference or pointer and returning a reference or pointer from a function.
The confusing part is the difference in the const
qualifier. Let’s look at some examples.
When we call Process(input);
in line 10
We make a copy of the input
object, there is no problem changing from non-const to const
Input
version as there are two copies. Another way is also correct:
The same goes for the return value, wherever we add the const
qualifier it will work because we make a copy.
When we deal with references, it’s different. When we change our function to accept const
reference the behavior remains the same, we can pass const
and non-const objects to it.
Both versions below work.
But when we change our function to accept the non-const reference shown below, we can only pass non-const objects to it.
This is because unlike passing by value, when we use a reference, we don’t make a copy, we use the same object. Therefore we cannot change the qualifier of an object from const
to non-const.
The following example is interesting.
In this example, we pass the const
reference on line 9
to a function that accepts an object by value. In this case, it works, even though we changed from const
to non-const.
It’s confusing because of the magic the compiler does. References are just pointers under the hood. The compiler does all the work to do the conversion for us. What happens, in this case, is the following:
The compiler modifies our code to pass-by-value, so in this case, we make a copy of it. That’s why it works. Just to complete the example, in the previous version above this is what happens after the compiler modifies our code:
It all becomes clearer when we see the pointer version.
Adding/Removing const qualifier with const_cast
In the section above we have seen that we can have a const
pointer or a const
reference that points to a non-const object, the other way around isn’t possible.
Now, if you have scenarios where you need to modify the constness, either way, you can use const_cast
. The following are some examples.
In this example, the library code allows us to modify value
for some reason. But, we decided that we don’t want to do it. In this case, we can add a const
qualifier to it by using const_cast
.
The other way is also possible, but not recommended.
This code works, because the original variable, value
at line 7
, is a non-const object. So we can remove the const
qualifier and modify the value. But, if the original variable is const
as shown in the code below, the result is undefined.
This code compiles without error, but the result is undefined. This example will print the following (tested with GCC and clang):
mutableValue: 100
value after callback: 10
Please note that, if we write our code right, we should never use const_cast
. The only use case is perhaps when we deal with library code that gives us a pointer to a non-const object and we want to make it safer by modifying it to a pointer to a const
object.
- The
const
qualifier is very important to express our intention to compilers and other programmers that we want the object to be read-only - The compiler helps us perform checks by throwing errors when there are attempts to change our read-only variables
- A
const
qualifier on a member function is just syntax for adding aconst
qualifier tothis
pointer - We can return a
const
value from a function to prevent temporary object modification, but doing so has the unintended impact of preventing move semantics - We can modify constness of an object using
const_cast
but valid use cases are very rare, for example when dealing with 3rd party legacy libraries that you can’t modify