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
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.
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
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:
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);
const T* to this function causes a compile error because we are trying to implicitly remove the
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.
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
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
constwe 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
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
Tyou 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
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
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):
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
constqualifier 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
constqualifier on a member function is just syntax for adding a
- We can return a
constvalue 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_castbut valid use cases are very rare, for example when dealing with 3rd party legacy libraries that you can’t modify