const qualifier does not always work the way we expect it to. In this article, we will see some cases when the const doesn't protect us from modifications at all.
The const
keyword is widely used within almost every C++ program. It symbolizes the idea of a constant variable, object, function, and more, and is meant to forbid us from manipulating and changing the content of a memory location within a certain scope. However, as we learned before, “There is nothing more deceptive than an obvious fact.” (Sherlock Holmes).
Const Rules
In this article, I won’t dive deeply into the const
rules, but just move briefly on const
ideas I consider as crucial for the point I want to talk about.
Let’s start with the basic rules of const
. The most basic const
example is a constant variable:
const int a = 42;
In this example, we initialize a constant integer with a value of 42
. Any const
variable should be initialized at the creation stage (due to the inability to initialize it later). Once the variable is initialized, there is no way to change it (Well… const_cast
but it didn’t get into this article scope).
Const Location
const
keyword can be written in multiple locations of the type:
const int a = 5;
int const b = 6;
The result is the same, in both cases, the variables are constant. However, this ability has a great impact when pointers join the picture.
int val;
const int* a = &val;
In this example, we initialize a mutable (not const
) int
variable named val
, and a const
pointer to it called a
. Now, I say const
because it’s only a partial const
pointer, here is actually applied only to the pointer’s value, not the pointer itself, which means that we can modify this pointer to point to another variable, but we won’t be able to modify the variable content through this pointer. For example:
int val;
const int* a = &val;
int val2;
a = &val2;
Now, this also means that we can create the pointer variable without assigning a value to it at the creation stage:
const int* a;
If we want to protect the pointer, we have to change the const
qualifier location. Let’s inspect the different possible locations for the const
qualifier in this example:
const int* a; int const* b;
int val;
int* const c = &val;
We can combine the different const
locations as long as they don’t repeat the meaning in the same variable:
int val;
const int* const a = &val; int const* const b = &val;
The rule is simple if a const
is the first qualifier in the type, the const
will be applied to the next element of it. Otherwise, it’ll always be applied to the previous component of it.
Side note: A constant pointer equivalent is a reference variable, for example:
int val;
int &a = val;
int *const b = &val;
Variables a
and b
here have the same meaning, they are both protecting the pointer from changes.
Class Const Member Functions
Now that we know the rules for constant variables, let’s talk about constant functions.
Constant functions are defined as functions that cannot modify a class’s variables. For example:
class A {
public:
void func() const {
}
private:
int a;
};
Everything seems great, right? Let’s inspect another case:
class A {
public:
void func() const {
}
private:
int *a;
int val;
};
So far, everything seems just right. If a function is const
, the class’s members can not be modified. It’s time to break this rule a little:
class A {
public:
A() : a(&val) {}
void func() const {
*a = 42; }
private:
int *a;
int val;
};
I guess it’s the right time to go back to the quote, “There is nothing more deceptive than an obvious fact.” of Sherlock. The constant function only protects the last element of the class’s members’ types. So in this example, we modified a member variable content through a pointer member inside a constant member function.
Marking this function as a const
function is a mistake that might lead to bugs if someone will relay on this const
qualifier. This function actually performs a hidden const_cast
and it shouldn’t. In order to fix this issue, the right thing would be to remove the const
qualifier from this function.
Code inspection tools sometimes might advise us to add a const
qualifier to this function, because the compiler allows that, but we shouldn’t. Adding a const
qualifier to a class’s function should always be done only after careful consideration of whether this function modifies the class’s state/variables or not.
Let’s see an even more confusing example:
class A {
public:
A() : a(val) {}
void func() const {
a = 42; }
private:
int &a;
int val;
};
This time, we even don’t see the *
when modifying the content. And it’s not even a UB! It’s totally legal by the language’s standard.
Conclusion
The title LConst PConst
stands for Logical Const vs. Physical Const. We should always prefer to use the logical one over the physical one, and the compiler doesn’t help us this time.
I didn’t dive into more aspects of the const
qualifier in this article, but I might do it in future articles. If you have a specific request, feel free to contact me on the dedicated channels of this blog like Discord and Telegram.
Feel free to leave your opinions in the comments section.