The use of const
keyword in C++ applies constraints to the program which will be statically checked by the compiler before program’s running. Therefore, it provides an additional layer of safety mechanism during our programming activity, such as preventing us from involuntarily modifying the value of a constant variable or getting its non-constant (mutable) reference. This will be extremely useful for us to develop and debug large scale numerical libraries in a defensive way, where mathematics and programming techniques are interwoven, which makes us more prone to error than traditional software containing only work flow of logics rather than mathematics. Therefore, it is meaningful to clarify and understand the key points of const
usage, which will be the topic of this article.
Differentiate between internal and external linkage of a const
variable
We often come across a scenario that a const
variable is declared and initialized at the same time in a header file, which will be included by many compilation modules, i.e. cpp files. Such variables are internally linked by the compiler, which are stored in the compiler’s symbol table and do not require memory allocation. This is called constant folding.
When a const
variable is declared with an extern
prefix in a header file but without initialization, the connotation of extern
means the actual definition of this const
variable is not in the same header file, but in some cpp file. Then, this variable is externally linked by the compiler, which is not stored in the symbol table of the compiler and requires actual memory allocation.
Associativity of const
keyword
const T * p
or T const * p
defines a pointer, which points to a const
value of type T
, while the memory address stored in p
itself can be modified.
T * const p
defines a pointer which points to a value of type T
. The difference from above is that the address stored in the pointer cannot be changed and the value stored in the variable of type T
can be modified.
const T * const p
defines a pointer which points to a const
value of type T
and the address in p
is also const
.
Anyway, the basic principle is that the const
keyword is bounded to its closest entity from the right and if const
is the left-most keyword, it is bounded from the left.
const
return value of a function
We should keep in mind that the direct return value of a function which has not yet been assigned to an external lvalue
is treated as const
. This means the following expression is wrong, i.e. the direct return value of a function cannot be overridden.
int foo();
foo() = 3;
Temporary objects
Temporary objects of a class are created by directly calling the constructor of the class. When it is used as an lvalue
, it is mutable. When it is used as a rvalue
, it is const
. When a temporary object is passed to a function as argument, it is treated as a rvalue
and hence has const
type.
The operator &
for extracting a variable’s memory address cannot be applied to a temporary object. Therefore, a temporary object can only be passed to a function by const
reference but not by address or mutable reference.
class ClassName;
void foo_pass_by_pointer(const ClassName *);
void foo_pass_by_const_reference(const ClassName &);
void foo_pass_by_reference(ClassName &);
// Invalid
foo_pass_by_pointer(&ClassName());
// Valid
foo_pass_by_const_reference(ClassName());
// Invalid
foo_pass_by_reference(ClassName());
Use the const
keyword in a class
const
member functions
Dichotomy between const
and mutable member functions
When the const
keyword is explicitly appended to both the declaration and definition of a member function in a class, the compiler is told that member variables of the class should not be changed during the execution of this member function. This explicit declaration is mandatory because the compiler has no knowledge about the constness of member variables during the function execution.
const
member functions are mandatory because they are the only interfaces for accessing constant objects. If they are not defined, the following compiler error will usually appear:
error: passing 'const ClassName' as 'this' argument of 'int& ClassName::member_function()' discards qualifiers [-fpermissive]
This can be understood like this: every member function in a class has an inherent first argument which is the this pointer pointing to the instance of the current object, as shown below.
result_type member_function(ClassName * this_pointer, ...);
This is a mutable version of member_function
.
When the const
keyword is appended to it,
result_type member_function(...) const;
it is a const
version of member_function
which is equivalent to declaring a function as
result_type member_function(const ClassName * this_pointer, ...);
Therefore, if there is only a mutable version of member_function
provided, when it is called from a const
object of ClassName
, there will be an implicit typecast from ClassName *
to const ClassName *
. This is forbidden by the compiler and the above “discards qualifiers” error message will appear. On the other hand, if there is only a const
version of member_function
provided, when it is called from a mutable object of ClassName
, there is no error, because typecasting from ClassName *
to const ClassName *
is valid.
After understanding this, when we design and implement a new class, it is a good practice for us to apply the dichotomy between const
and mutable to all class member functions.
Return value of a const
member function
If the returned value of a const
member function is a reference to some member variable in the class, because all member variables have been considered as const
by the compiler inside the const
member function, the returned reference should also be const
.
Access class static variables from a const
member function
A const
member function only ensures that no changes will happen to class member variables. Therefore, class static variables, which do not belong to any instance of the class, can be modified within a const
member function.
const
member variables
Non-static member variables
When a non-static member variable of a class is declared as const
, it is not required to be initialized with a value at the same time as that required for a global const
variable. The const
keyword here only means that once this member variable is initialized, its value cannot be changed any further. Still, it is grammatically correct that such initial value is given at the declaration of a non-static member variable. However, in this way, each instance of this class will have the same value for this variable which cannot be modified. Then, there is actually no need to keep a same copy for it in each instance and we should define it as a const
static member variable instead.
The appropriate place for initializing a const
non-static member variable is the initializer list of the class’s constructor. This is because before the execution of the constructor’s function body, member variables of the class have already been initialized according to the initializer list. Therefore, it is the only chance to assign value to a const
non-static member variable in the initializer list.
It should also be noted that constant folding is not applicable to a const
non-static member variable. Meanwhile, because it cannot be guaranteed to be initialized with a value, it cannot be used to specify the size of an array.
Static member variables
A const
static member variable in a class is shared by all the instances of the class. It is stored in the symbol list without memory allocation. Hence, constant folding is enabled for it and it can be used to specify the size of an array. There are two ways to initialize a const
static member variable.
-
Initialized at the declaration, i.e. in situ initialization.
class ClassName { static const int size = 100; };
-
Initialized outside the class, which is usually placed in the corresponding cpp file for the class.
const int ClassName::size = 100;
One thing should be noted that because member variables of a class are initialized in the same order as their declaration, we have to make sure that the declaration and initialization of the static const
member variable should be in front of the array whose size is defined via this variable.
Good practices of using const
-
Whenever a reference or a pointer is passed to a function, it should be declared as
const
in the function’s signature as much as possible. This is due to the following considerations.- Enable the function to be able to operate on a
const
object, because it is impossible to cast a type fromconst
to mutable, unlessstd::const_cast
is used. - Stick to the spirit of defensive programming and avoid any unnecessary or careless modification of the variable.
- Enable the function to be able to operate on a
-
Static
const
member variable had better be initialized in situ. -
Always keep in mind the dichotomy between
const
and mutable member functions when designing a class. For class member functions which never change member variables, they should be declared asconst
, so that they can be used for both mutable andconst
object.