const
Correctnessconst
and why we should use it throughout our application.Following on from our lesson on the many uses of const
, lets discuss when we should, and when we shouldn't, make use of it.
In the below example, as y
was not marked as const
.
int Add(const int& x, int& y) {
return x + y;
}
int x = 1, y = 2;
Add(x, y);
But, it is easy to tell from this simple example that y
will never be changed. We just didn't add the const
directive to make that clear.
Our code is valid, but not "const correct".
In general, we should always try to add the const
(or constexpr
) keyword where it is useful. The usefulness of const
depends greatly on context.
The above example - passing a reference or a pointer to a function - is the scenario where being const
correct is most important.
This is because when someone passes a variable they created to a function, they are likely going to be very interested in whether or not our function is going to modify their variable. That could have implications on how they write the rest of their function, because they may need to create additional code to deal with that possibility their variable has changed.
If we write a function that accepts a pointer or reference argument, and we do not mark that argument as const
, anyone who calls our function will have a reasonable expectation that their variable could be modified.
If that is not true, and in reality we just neglected to specify the parameter as const
, our omission could indirectly create extra work and performance overhead.
const
Even though there are many situations where we can use const
, it's not always a good idea.
Firstly, there's almost 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 like we might expect. It can also degrade performance. We cover this in more detail later.
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 fairly common to not mark these argument types as const
, even if they're not being modified.
Here, both our parameters could be const
, but is it really worth making our function signature more complicated?
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, so is it really worth it?
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.
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 inevitely 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 like this:
// Cannot modify this
namespace SomeLibrary {
int Double(int& x) {
return x * 2;
}
}
int MyFunction(const int& x) {
return SomeLibrary::Double(x);
}
We have a function receiving a constant reference. We want to pass that reference into a function that will not modify it, but that function has not formalised that by marking the parameter const
.
As such, our call to the library function will throw an error.
For these situations, we have const_cast
. This will remove the "constness" of a reference, or of the value pointed to by a pointer.
int x { 5 }
const int* Pointer { &x };
const int& Reference { x };
int* NonConstPointer { const_cast<int*>(Pointer) };
int& NonConstReference { const_cast<int&>(Reference) };
Following the above code, any function expecting a pointer or reference to a non-const version of x
can be given NonConstPointer
or NonConstReference
respectively.
Updating our previous example to use const_cast
would look something like this:
// Cannot modify this
namespace SomeLibrary {
int Double(int& x) {
return x * 2;
}
}
int MyFunction(const int& x) {
return SomeLibrary::Double(
const_cast<int&>(x)
);
}
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 implicitely converted to a const reference.
Relying on this implicit cast is generally preferred in this scenario, compared to the more verbose explicit casting.
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way