Understanding All the Details of C++ Const | by Debby Nirwan | Feb, 2022

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

Debby Nirwan
Photo by Zanny Jadraque on Unsplash

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 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 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 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 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 a const qualifier to this 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_castbut valid use cases are very rare, for example when dealing with 3rd party legacy libraries that you can’t modify

Leave a Comment