const
-Correctnessconst
and how to apply it in different contextsIn our original lesson on references, we introduced the concept of pass-by value. This means that, when we call a function, the parameters are generated by creating copies of the arguments. Below, MyFunction()
receives a copy of Player
.
class Character {/*...*/};
// Passing by value
void MyFunction(Character Input) {}
int main() {
Character Player;
MyFunction(Player);
}
When our parameters are non-trivial, this can be problematic, as copying a complex object has a performance overhead. Additionally, it’s quite unusual for our functions to actually require a copy. Therefore, we prefer to pass objects by reference (or pointer) where possible.
Below, MyFunction()
and main()
share the same copy of Player
. MyFunction()
can access Player
through the Input
reference:
class Character {/*...*/};
// Passing by reference
void MyFunction(Character& Input) {}
int main() {
Character Player;
MyFunction(Player);
}
const
-CorrectnessHowever, passing by reference opens up another problem. When we pass an object to a function by value, we know our object is not going to be modified by that function call. Any action the function performs on the object will be performed on a copy - our original object will be safe.
When we pass by reference, that is no longer the case. The function could modify our object, so we need to consider what state our object could be in after our function call:
class Character {/*...*/};
void MyFunction(Character& Input) {}
int main() {
Character Player;
MyFunction(Player);
// What is Player's health now?
}
Having to read through a function’s body (or documentation) to understand what effect it can have on our variable is quite annoying. Fortunately, in almost all cases, a function will not change the object at all. It is simply receiving it by reference to avoid the performance impact of creating a copy.
In these scenarios, we can mark the parameter as const
. This communicates to the caller that their variable isn’t going to be modified, and asks the compiler to take steps to ensure that it is not modified.
class Character {/*...*/};
void MyFunction(const Character& Input) {}
int main() {
Character Player;
MyFunction(Player);
// What is Player's health now?
}
When our code correctly marks variables that won’t be changed as const
, it is considered ***const
-correct***.
const
after the typeIt is also valid to place the const
qualifier after the type it applies to, although this pattern is less commonly used:
void MyFunction(Character& const Input) {}
The const
keyword is an abbreviation of constant - a constant simply refers to something that doesn’t change. Phrases like "immutability" are sometimes used to refer to the same concept.
Changing an object is sometimes referred to as mutating it. An object that can change is mutable, whilst an object that cannot change, ie a constant, is immutable.
const
Member FunctionsIn this lesson, we will explain how they can be used in many other situations. The const
keyword can be used in a lot of different ways in C++, and they interact to help us fully implement const-correctness.
In our previous example, we marked our Character
as const
, meaning the compiler will stop us from doing anything that could change it. This includes calling any functions on the object. The following program will generate a compilation error when we try to call GetHealth()
on our const
reference:
#include <iostream>
class Character {
public:
int GetHealth() { return mHealth; }
void SetHealth(int Health) {
mHealth = Health;
}
private:
int mHealth{100};
};
void MyFunction(const Character& Input) {
std::cout << "Health: " << Input.GetHealth();
}
int main() {
Character Player;
MyFunction(Player);
}
In this class, GetHealth()
isn’t modifying the object - it’s simply reading a variable. So, we should mark that function as const
too. This asks the compiler to ensure nothing in the GetHealth()
function body modifies our object:
#include <iostream>
class Character {
public:
int GetHealth() const { return mHealth; }
void SetHealth(int Health) {
mHealth = Health;
}
private:
int mHealth{100};
};
void MyFunction(const Character& Input) {
std::cout << "Health: " << Input.GetHealth();
}
int main() {
Character Player;
MyFunction(Player);
}
A side effect of this is that it also fixes the compilation error we had. Since we declared the GetHealth()
function does not modify the object, we can now call it with a const
reference. Our previous program now compiles successfully, and outputs:
Health: 100
const
VariablesWe’re not restricted to just using const
with references. We can declare any variable as const
:
int main() {
const int Health{100};
Health++;
}
error: expression must be modifiable
Member variables can also be const
:
class Character {
public:
const int Level{1};
};
int main() {
Character Player;
Player.Level++;
}
error: 'Player': you cannot assign to a variable that is const
Additionally, identifiers that point to complex objects can be const
, even if they’re not references:
class Character {
public:
int Level{1};
};
int main() {
Character PlayerOne;
PlayerOne.Level++; // this is fine
const Character PlayerTwo;
PlayerTwo.Level++; // this is not
}
error: 'PlayerTwo': you cannot assign to a variable that is const
By marking a variable as const
, the compiler will also prevent us from creating a reference to that variable, unless the reference is also const
:
class Character {
public:
int Level{1};
};
// Passing by non-const reference
void MyFunction(Character& Input) {}
int main() {
Character PlayerOne;
MyFunction(PlayerOne); // this is fine
const Character PlayerTwo;
MyFunction(PlayerTwo); // this is not
}
error: cannot convert argument 1 from 'const Character' to 'Character &'
Finally, we can copy a const
variable to a non-const
variable. This is most relevant when passing a const variable by value to a non-const
function parameter. This is allowed because, within the function, any modifications would be done to a copy.
As we’ve seen before, the original const
variable is not being modified:
#include <iostream>
void MyFunction(int Number) {
// incrementing a copy
Number++;
}
int main() {
const int Number{5};
// Passing a const variable by value to a
// non-const function parameter is fine
MyFunction(Number);
std::cout << "Number is still " << Number;
}
Number is still 5
const
PointersAn interesting property comes up when considering const
from the perspective of pointers. This is because the pointer, and the thing it is pointing at, are two separate objects.
Therefore, our use of const
in this context can take four different forms:
const
When neither the pointer nor the object being pointed at are const
, we can modify either of them. Below, we update our object through the pointer, and then we update the pointer itself. Both are allowed:
int main() {
int Number{5};
int* Ptr{&Number};
// We can modify the object
(*Ptr)++;
// We can modify the pointer
Ptr = nullptr;
}
const
When we mark the pointer as const
, we cannot update it. However, we can still update the object it is pointing at. We mark the pointer as const
by placing the const
keyword after the *
on the type:
int main() {
int Number{5};
int* const Ptr{&Number};
// We can modify the object
(*Ptr)++;
// We can NOT modify the pointer
Ptr = nullptr;
}
A pointer of this type is colloquially referred to as a "const pointer"
When the object being pointed at is const
, we can’t update it through the pointer, but we can update what the pointer is pointing at. We mark the object as const
by placing the const
keyword at the start of the type:
int main() {
int Number{5};
const int* Ptr{&Number};
// We can NOT modify the object
(*Ptr)++;
// We can modify the pointer
Ptr = nullptr;
}
A pointer of this type is colloquially referred to as a "pointer to const"
Finally, it is possible for both the pointer and the object being pointed at to be const
. We implement this using the const
keyword twice - once before the *
and once after:
int main() {
int Number{5};
const int* const Ptr{&Number};
// We can NOT modify the object
(*Ptr)++;
// We can NOT modify the pointer
Ptr = nullptr;
}
A pointer of this type is colloquially referred to as a "const pointer to const"
int
: int* Num
const
pointer to int
: int* const Num
const int
: const int* Num
const
pointer to const int
: const int* const Num
This syntax is very likely to be confusing, and nobody is expected to remember it. The important thing to remember is the concept - the pointer and the object being pointed at are two different things, and either of them can be const
.
const
after the type (with pointers)When we’re using the less common approach of placing the const
qualifier after the type it applies to, a pointer to const
would look like this:
// Equivalent to const int* Ptr;
int const* Ptr;
A const
pointer to const
would look like this:
// Equivalent to const int* const Ptr;
int const* const Ptr;
const
Return TypesFunction return types can be marked as const
. However, there are some implications here we should be aware of. We can imagine the caller will receive the objects returned by functions by value, so they will receive copies of the object.
Therefore, the value received by the caller will not be const
, unless the caller marks them as const
:
const int GetNumber() {
return 1;
}
int main() {
int A{GetNumber()};
A++; // This is fine
const int B{GetNumber()};
B++; // This is not
}
And if the caller marks them const
, they will be const
regardless of what the function prototype declared. The net effect of this is that there is no reason to mark function return types as const
.
Additionally, marking return types as const
can impair performance, as it interferes with return value optimization - a compiler feature we’ll cover in detail in the next course.
Note this advice only applies to values returned from functions. It is reasonable for functions to return a constant reference if needed:
class Character {};
Character Player;
const Character& GetPlayer() {
return Player;
}
int main() {
const Character& PlayerPointer{GetPlayer()};
}
Similarly, returning a pointer to const can make sense:
class Character {};
Character Player;
const Character* GetPlayer() {
return &Player;
}
int main() {
const Character* PlayerPointer{GetPlayer()};
}
constexpr
We can imagine there being two types of constant variables - those that are known when we build our software, and those that can't be known until our software runs.
Almost all of the variables we've seen so far have been known at compile time and could be compile-time constants. An example of a compile-time constant is something like this:
const float Gravity { 9.8 };
Many other variables cannot be known until the program is run. Examples of these variables might include:
The more things that can be done at compile time, the more performant our software will be. Compilers can often detect when our code is creating compile-time constants, and optimize them for us automatically.
However, we can make sure that a variable is a compile-time constant (and have the compiler alert us if it doesn't seem to be) by using the constexpr
(short for constant expression) keyword.
constexpr float Gravity { 9.8 };
const
Even though there are many situations where we can use const
, it's not always a good idea.
As we covered above, there's no reason to use const
for a value returned by a function:
const int Add(int x, int y) {
return x + y;
}
Due to the way values are returned from functions, the use of const
in that context does not behave as we might expect. It can also degrade performance.
Also, const
is less important for arguments that are passed by value. Passing by value means the incoming data is copied. Callers of a function are generally not going to care about what happens to a copy of their data.
As such, it is not particularly important whether we mark these as const
. Below, both our parameters could be const
, but many would argue it would just be adding noise to our code:
int Add(int x, int y) {
return x + y;
}
Similarly, it may not be worth marking a simple local variable as const
. In the following example, Damage
could be const
, but the variable is discarded after two lines of code:
void TakeDamage() {
int Damage { isImmune ? 0 : 100 };
Health -= Damage;
}
Different people and companies will have different opinions on these topics.
The Unreal coding standard asks for const
to be used anywhere it is applicable:
Const is documentation as much as it is a compiler directive, so all code should strive to be const-correct. ... Const should also be preferred on by-value function parameters and locals. ... Never use const on a return type. […] This rule only applies to the return type itself, not the target type of a pointer or reference being returned.
Google's style guide is less prescriptive, suggesting we "use const
whenever it makes sense":
const
with const_cast
Whilst we should try to make our code const correct, we're inevitably going to find ourselves interacting with other code that isn't.
When this happens, we'd ideally want to update that other code, but that isn't always an option. For example, that code might be in a 3rd party library that we're not able to modify.
This can leave us in a situation where we know a function isn’t going to modify a parameter, but they have neglected to mark it as const
:
// Assume we can't change the code in this
namespace SomeLibrary {
// This function doesn't modify Input,
// but has not marked it as const
float LogHealth(Character& Input) {
std::cout << "Health: "
<< Input.GetHealth();
}
}
Meanwhile, our const-correct code wants to use this function, but the compiler will prevent us from doing so because we can’t create a non-const
reference from a const
reference::
#include <iostream>
class Character {/*...*/};
namespace SomeLibrary {/*...*/};
void Report(const Character& Input) {
SomeLibrary::LogHealth(Input);
}
int main() {
Character Player;
Report(Player);
}
error: 'float SomeLibrary::LogHealth(Character &)':
cannot convert argument 1 from 'const Character' to 'Character &'
For these situations, we have const_cast
. This will remove the "constness" of a reference or the value pointed to by a pointer.
int x { 5 }
const int* Pointer { &x };
int* NonConstPointer {
const_cast<int*>(Pointer)
};
const int& Reference { x };
int& NonConstReference {
const_cast<int&>(Reference)
};
We can update our previous program to make use of this:
#include <iostream>
class Character {/*...*/};
namespace SomeLibrary {/*...*/};
void Report(const Character& Input) {
SomeLibrary::LogHealth(
const_cast<Character&>(Input)
);
}
int main() {
Character Player;
Report(Player);
}
Health: 100
const
using const_cast
const_cast
can also be used to add constness to a reference or pointer:
int x { 5 };
int& Reference { x };
const int& ConstantReference {
const_cast<const int&>(Reference)
};
However, this is generally not useful. We've already seen how a non-const
reference can be implicitly converted to a const
reference.
mutable
MembersWithin our classes, we can mark some variables as mutable
. Mutable variables can be modified by const
member functions.
The use case for this is inherently quite niche and, oftentimes, using it at all indicates a flaw with our design. This is particularly true if we're marking variables that are core to the functionality of our class mutable
.
However, it can sometimes be helpful for scenarios where our objects carry ancillary data that is not part of the public interface.
Below, we would like to keep track of how many times consumers have checked the health of our object, so we introduce a private member variable and increment it from GetHealth()
:
class Character {
public:
int GetHealth() const {
HealthRequests++;
return Health;
}
private:
int Health { 100 };
int HealthRequests { 0 };
};
Naturally, the compiler prevents this, as GetHealth()
is const
, and we can’t modify our object from a const
function:
error 'HealthRequests' cannot be modified because it is being accessed through a const object
We could make GetHealth()
non-const
, but that could be quite disruptive, requiring us to make lots of Character
references around our code base non-const
too.
Additionally, it would be a confusing change for consumers. Whilst GetHealth()
is indeed modifying the object, it’s not modifying it in a way that consumers should ever care about.
So, instead, we can simply mark the HealthRequests
variable as mutable
:
class Character {
public:
int GetHealth() const {
HealthRequests++;
return Health;
}
private:
int Health { 100 };
mutable int HealthRequests { 0 };
};
This lesson introduces the concept of const
-correctness, exploring how const
can be used with variables, functions, and pointers to prevent unintended modifications.
Key Takeaways:
const
can be used to avoid modifications to the original object when passed by reference.const
member functions and how they ensure member methods do not modify the object, allowing their use with const
objects.const
pointers and const
objects, including various combinations like const
pointer to const
and const
pointer to non-const
.constexpr
) versus runtime constants (const
) and their respective uses.const
and understanding advanced concepts like const_cast
and mutable
members.In the upcoming lesson, we will delve into some commenting best practices. Additionally, we'll introduce comment formats like Javadoc, which makes our comments more powerful.
Key Topics Covered:
const
-CorrectnessLearn the intricacies of using const
and how to apply it in different contexts
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way