Everything About Consts in C++

Gagan Saini
The Startup
Published in
5 min readSep 19, 2020

One-liner: const is used to declare constants in C++. But there are a lot more details about using this simple yet powerful keyword. Let’s dig deeper.

const values

We can declare constant variables (or more specifically ‘values’) by decorating them with const keyword as follows:

const int Max = 256;//we must initialize it.
Max++;//compiler error

What’s wrong with #define?

We can also declare constants using the old #define way.

#define MAX 256 

But it has its own share of problems:

  • It does not introduce a name. In the above example, the name ‘MAX’ will never be seen by the compiler as the pre-processor will replace all the instances of ‘MAX’ with 256.
    This may create some confusion if we get an error during compilation involving the use of this constant, as the error message may refer to 256, instead of the name ‘MAX’.
  • Similar problem can also crop up in a symbolic debugger, because, again, the name we’re programming with may not be in the symbol table.
  • ‘MAX’ will be visible to entire program even if it is defined inside a block. With the const values, we can apply scope rules and class access specifiers like public, private etc.

const with pointers

Non Constant pointer. Constant data object.

When the const keyword is on the left side of *. The following two lines does the same thing.

const int *ptr = &x;
int const *ptr2 = &x;

Here, the pointer can be modified to point to any other data item of appropriate type, but the data to which it points cannot be modified through this pointer. We can say ptr is a pointer to a constant integer.

For example:

int x = 1, y = 2;
const int *ptr = &x;
*ptr = 3;//Compiler Error
ptr = &y;//OK. The pointer is not constant

Constant pointer. Non Constant data object.

When const keyword is on the right side of *.

int * const ptr = &x;

Here, we cannot modify the pointer to point to any other data item. But we can modify the contents of the object to which it points. We can say ptr is a constant pointer to an integer.

For example:

int x = 1, y = 2;
int * const ptr = &x;
*ptr = 3;//OK. The data is not constant
ptr = &y;//Compiler Error

A const pointer must be initialized when it is declared.

A constant pointer to a variable is useful for the storage that can be changed in value but not moved in memory.

Constant pointer. Constant data object.

When const keyword appears on both the left and right sides of *.

const int * const ptr = &x;

Here, we cannot modify anything. Neither the pointer nor the data object it points to. We can say ptr is a constant pointer to constant integer.

For example:

int x = 1, y = 2;
const int * const ptr = &x;
*ptr = 3;//Compiler Error
ptr = &y;//Compiler Error

const and casting

We cannot create a regular pointer to a const object. It must be a pointer to const as shown below:

const int x = 1;
int * ptr = &x;//Compiler Error
const int *ptr3 = &x;//OK

const_cast

const_cast can be used to take away the constness of a const object. const_cast can only be applied to pointers and references.

const Employee emp;
emp.id = 123;//Compiler Error
Employee *ptr = const_cast<Employee *>(&emp);
ptr->id = 123;//Ok

It is undefined behavior to modify a value which is initially declared as const.

const int i = 34;
int *ptr = const_cast<int *>(&i);
*ptr = 5;//Undefined behavior

Note that in the previous example, when we modified emp.id, that time we did not get undefined behavior. Because we modified emp.id which is not a const.

Some use cases of using const_cast:

  1. Modify some data of the class inside a const member function by const-casting the ‘this’ pointer. But we have more elegant solution for that — mutable.
  2. Passing a const object to a function expecting non-const object.

const with references

We can have a reference to constant data as follows

int x = 0;
const int& ref = x;
int const& ref2 = x;//same as ref
ref = 4;//Compiler Error
ref2 = 4;//Compiler Error

What happens if we write const after & ?

int& const ref = x;
ref = 4;//OK

Nothing happens. All references in C++ are implicitly constant. i.e. after a reference initialization, we cannot modify it to point to some other data object.

const with functions

const reference parameters

By default, C++ passes objects to and from functions by value. So it involves calling of copy constructors of the parameters and then their destructors when exiting the function. This can have significant overhead.

To avoid this, we pass the parameters by reference, which exposes a possibility of them being modified in the function. So we mark them as const as shown in the example below.

bool compare(const Employee& emp1, const Employee& emp2) { ... }

const return value

When the function is returning some array pointer or c style string (char pointer), we may introduce some errors unknowingly. One such example is below.

At line 7, I mistyped == as =, but it still works. A value of true gets assigned to arr[0]. To avoid such errors, we may want to decorate the function return type with const. As in func2() above.

const with classes

const objects

The objects declared as const, can only call the methods which do not modify any fields of the class. Such methods are also need to be decorated with const keyword.

const member functions

A const method cannot modify the data members of the class. e.g.

void Employee::display() const
{
...
}
void Employee::calculateSalary()
{
...
}
const Employee emp1;
Employee emp2;
emp1.display();//Ok
emp2.display();//Ok. A non const object can call const method
emp1.calculateSalary();//Error. Const obj calling non-const method
emp2.calculateSalary();//Ok

The type of ‘this’ pointer in a const member function is const-const (i.e. const pointer. constant data). So this method cannot change any data that belong to ‘this’.

A const member function can be called by any object (either const or non const) of that class.

We can have two types of constness:

  1. Bitwise constness : When even a single bit of the object can not be modified.
  2. Logical constness : When some part of the object can be modified and rest of it remains constant.

Let’s see an example of logical constness. Searching through the linked list does not require modifications to the data of the linked list, so the search() could be a const method of the linked-list class. However, to make future searches faster, the linked-list object might keep track of the location of the last successful match. To do this, the const search() must be able to modify the data member that keeps track of the last successful search. Welcome to the mutable keyword.

mutable

A mutable data member can be modified by a const method or a const object.

We are able to modify readCount variable through getValue() method and MyInteger’s const object directly.

constness can be overloaded

We can have two methods with same signature with the only difference being the constness.

void Employee::display() const {...}
void Employee::display() {...}
const Employee emp1;
Employee emp2;
emp1.display();//will call the const method.
emp2.display();//will call second one.

--

--