const
and constexpr
const
and constexpr
keywords to make our code betterConstants are things that do not change. In C++, we generally express that requirement with the const
keyword.
In 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++. The exact effect depends on the context. Lets see some examples.
const
Member FunctionsLets imagine we have this class:
class Character {
public:
int GetHealth() {
return Health;
};
private:
int Health { 100 };
};
In the above example, our class has a GetHealth()
function that does not modify any properties of the Character
object.
These type of functions can therefore be marked as const
.
We can do this by adding the const
keyword to the function signature:
class Character {
public:
int GetHealth() const {
return Health;
};
private:
int Health { 100 };
};
When declaring and defining a function seperately, such as in a header and cpp file, the const
specifier works in the same way as other keywords, like virtual
and override
.
That means, we only need to make the declaration as const
:
// Character.h
class Character {
public:
int GetHealth() const;
private:
int Health { 100 };
};
// Character.cpp
int Character::GetHealth() { return Health; }
Marking a function as const
achieves two main goals:
const
with C++ VariablesWe can declare any variable as const
:
const float Armour { 0.2f };
// Cannot change a const variable
Armour = 0.5f;
Member variables can also be const
:
class Character {
public:
const int Level { 1 };
};
Character Frodo;
// Not allowed - Level is const
Frodo.Level++;
const
with C++ ObjectsEntire objects that we create from a class can be const
. With const
objects, we cannot change any variables, even if the class defines them as public and not const
.
We also cannot call any functions on that object, unless the function is also const
.
class Character {
public:
int Health { 100 };
void TakeDamage(int Damage) { Health -= Damage; }
void GetHealth() { return Health; }
float GetArmour() const { return 0.2; }
};
const Character UnchangableCharacter;
// Not allowed as the object is const
UnchangableCharacter.Health -= 50;
// We also can't call a function that is not const
UnchangableCharacter.TakeDamage(50);
// GetHealth doesn't change the object
// But is not marked as const, so we can't call it
UnchangableCharacter.GetHealth();
// But we can call a const function
UnchangableCharacter.GetArmour();
mutable
MembersWe can specify the mutable
keyword on any variable defined within our classes. Mutable variables can be modified by const
functions.
The use case for this is inherently quite niche and, often times, 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 quick debugging, or if our classes carry ancillary data.
For example, we could keep track of how many calls are made to our const
function like this:
class Character {
public:
int GetHealth() const {
HealthRequests++;
return Health;
}
private:
int Health { 100 };
mutable int HealthRequests { 0 };
};
const
C++ ParametersOur sections that talked about passing function arguments as references mentioned the ability to pass those references as const
. We can do the same with all function arguments, including values and pointers.
void TakeDamage(const int Damage, const Character* Instigator);
In the body of this function, we can not change Damage
or Instigator
The const
keyword is most important when dealing with function arguments that are references or pointers. When we pass a parameter by value, our function creates a copy of that variable.
Code calling our function is generally not going to care what happens to a copy of it's data. Therefore, it's fairly common to not mark these things as const
, even if they aren't being modified.
const
C++ ReferencesAs we've seen earlier, passing a reference to a function allows that function to modify a value that is outside of its scope.
void IncrementByRef(int& x) {
x++;
}
When we, as developers, write code that passes a reference to a function, one of the first things we will want to know is whether our value is going to be modified as a result of passing its pointer to that function.
We can use const
references in the parameter list to clarify whether or not that is going to happen.
void SomeFunction(const int& x) {
// We can't modify the value at x
x++; // not allowed
}
When we see a function that accepts a reference to a const
value, we know that our value is unlikely be changed as a result of that function call.
void SomeFunction(const int& x) {};
int x = 1;
SomeFunction(&x);
In the above example, we know the value contained in x
won't be changed by the call to SomeFunction()
, because SomeFunction
has said it will be treated as a reference to a const
variable.
Note, we didn't initialise x
as a constant. In our call to SomeFunction
, we are passing it a non-constant integer, but SomeFunction
's argument list is saying it will be a reference to a const
integer.
This is totally fine - our function is just going to treat the reference as if it is a const
int, even if x
wasn't initialised as a const.
The use of const
in this example is a promise that, whether x
is const or not, it will not be modified as a result of calling this function.
Note, the opposite is not true. Had we initialised x
as a const int
, we can only pass it as a reference to parameters that are explicitly marked as const
. The below example violates this, and will therefore not compile:
void SomeFunction(int& x) {}
const int x = 1;
SomeFunction(&x);
Hopefully this makes sense from a logical perspective. The first scenario is passing a variable that can be modified to a function that is promising not to modify it. There is no conflict there.
However, in the second example, there is a problem. The variable type says the value can't be modified, but the parameter type suggests the function might try to. So, the compiler objects to this conflict.
const
C++ PointersAn interesting property comes up when considering const
from the perspective of pointers. Consider the following code:
1int x { 100 };
2int y { 200 };
3const int* Pointer { &x };
4
5*Pointer += 50;
6Pointer = &y;
7
Line 5 is prevented as we might expect. But, perhaps surprisingly, line 6 is totally acceptable.
When we put const
before the *
, we are saying that the value being pointed at cannot be changed through the pointer (line 5) but the pointer itself can be updated to point at something else (line 6).
This is also the case if we put the const
between the data type and the *
. The following two statements are equivalent:
const int* Pointer { &x };
int const* Pointer { &x };
What we are creating here is sometimes called a "pointer to a const int". The integer is constant, but the pointer is not.
What if we wanted the opposite? A "constant pointer to an int"?
If we wanted to prevent the pointer from being updated to point to something else, we would move the const
after the *
:
int x { 100 };
int y { 200 };
int* const Pointer { &x };
*Pointer += 50;
Pointer = &y;
Now, we can change the value being pointed at (line 5) but can no longer make the pointer point at something else (line 6).
What if we wanted both to be constant? That is, what if we wanted a "constant pointer to a constant int"? We do this by using the const
specifier twice in our data type:
int x { 100 };
int y { 200 };
const int* const Pointer { &x };
*Pointer += 50;
Pointer = &y;
To summarise:
int
: int* MyNum
const
pointer to int
: int* const MyNum
const int
: int const* Num
const
pointer to const int
: int const* const MyNum
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 therefore 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 };
With all the different uses of const
under our belt, lets use the next lesson to briefly talk about the scenarios we should use it, and some scenarios where we shouldn't.
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way