C++, among its dozens of keywords, has a funny little keyword inherited from C named const. It is short for constant
and it is a qualifier that tells the compiler that some value does not change. const as a concept exists only for
the compiler and for us writing the code. There is no such thing as a const variable in the machine code, that
the compiler generates. const is a tool for the programmer to help keep the code correct and manageable. That is why,
in this article, I would like to present to you, in order:
- when
constshould always be used (and a similar keyword to use instead!), - when it is beneficial to use,
- when using it is possible, but counter-productive,
- when it might be a bad idea to use and, finally,
- when to never use
const.
Actual Constants
Often, we find ourselves writing a piece of logic that contains some magic numbers. Let's say we're writing an update function, called repeatedly, for an object that needs to smoothly bob up and down. We would write something like this:
void update_position() {
position.y += sin(elapsed_time * 2.0) * 12.0;
}
But we don't like magic numbers, so we go ahead and put them into variables. Here, we should not even think about
using const, because we should automatically use it without thinking!
void update_position() {
const double SPEED {2.0};
const double HEIGHT {12.0};
position.y += sin(elapsed_time * SPEED) * HEIGHT;
}
That looks much better. It is clearer now what those numbers mean and it might tell us that the numbers are chosen arbitrarily and that they are configurable. It is much safer now to modify the movement of the object.
There is no point in not making actual constants const and not writing them in a special case, that is
SCREAMING_SNAKE_CASE.
Pointers and References
Another place we should definitely use const is with pointers and references. const, here, actually has a slightly
different meaning, because it tells us and the compiler that we can't modify the referent through that reference, but
the actual value, the referent, could in fact be mutable. Most often we use references as function parameters,
in order to pass values to functions as arguments. Often, we need to modify those objects, to read and write to them,
so we use a mutable reference:
struct Vec2 {
double x, y;
};
struct Object {
Vec2 position {};
};
void reset_position(Object& object) {
object.position = {};
}
Other times, however, we only need to read an object, thus we use const!
double get_position_view_space(const Object& object) {
return object.position - camera_position;
}
const is like a contract that promises that an object will not be modified when passed to a function, for example.
When I see this:
void do_something_with_data(void* data);
Without reading the function body, I immediately know that whatever I pass to data, it might get modified. If, however,
the parameter were const instead, then I could stay in peace that my data is not going to be modified. It is like
a promise.
void do_something_with_data(const void* data);
This is incredibly important for good APIs. const for references is not just aesthetics! We might still
find old C APIs that do not respect const correctness. We might need to pass
a constant value to a function that does not modify the value, but it incorrectly receives it as a mutable reference:
typedef struct {
int x;
float y;
} Object;
int read_object(Object* object) {
return object->x;
}
In this case, in our C++ code, we'd have to use const_cast in order to remove the const qualifier from the pointer:
int main() {
const Object obj {5, 3.14159f};
// read_object(&obj); Doesn't work
read_object(const_cast<Object*>(&obj));
}
But we'd have to be very sure that the function really doesn't modify the value, otherwise it would be undefined behavior.
We can use const_cast to remove const from references and pointers in any way as long as we don't actually
try to modify any constant referent object.
Finally, marking reference parameters as const allows us to pass temporary values to those functions:
void print_hello(const std::string& name) {
std::cout << "Hello, " << name << "!\n";
}
int main() {
print_hello("Simon"); // Only allowed because name is a constant reference
}
Member Functions
Member functions, also known as methods, can be marked as const. In that case, all it does is in fact to const-qualify
the this pointer:
struct Object {
void non_const_function(int, float);
void const_function(int, float) const;
};
// Same as if:
struct Object {
void non_const_function(Object* this, int, float);
void const_function(const Object* this, int, float);
};
So, we should mark member functions that do not modify the instance as const!
Local Variables
Const-qualifying local variables is not as important as in the other cases previously presented, because they do not form an API as functions do. They are usually short-lived and contained into small functions or scopes:
std::string get_name();
Object create_object(const std::string& description);
void do_something_with_object(const Object& object);
int main() {
// Long line of code
const std::string obj_description {"Object(" + get_name() + ") is a class used to do this thing and..."};
const Object obj {create_object(obj_description)};
do_something_with_object(obj);
// More code here...
}
I personally do mark almost every local variable as const and strongly advice you to do the same,
but if you don't, it's not a disaster.
Not all local variables should be const, however... Consider an object that is non-trivial and a function that always
expects a temporary object:
struct Object {
Object(int x, float y);
~Object();
Object(const Object& other);
Object& operator=(const Object& other);
Object(Object&& other);
Object& operator=(Object&& other);
};
void do_something_with_object(Object&& object);
int main() {
Object obj {5, 10.0f};
// Lots of code here...
do_something_with_object(std::move(obj));
// Lots of code here too...
}
obj could very well be const, but shouldn't, because it is moved into the function. If obj were const instead, it
would be copied into the function, which is exactly what we don't want. So, don't mark objects that are going to be
moved as const!
Function Parameters
Function parameters are a special case of local variables, the difference being that they are usually not allocated
onto the runtime stack, but are copied into registers. They can be const just as well. I personally don't do that, just
because it makes function declarations and definitions way too long:
// Imagine, besides this, writing const five times
// I would personally write each parameter on its own line anyway
SomeObject some_function(const unsigned char* some_variable1, int some_variable2, float some_variable3, double some_variable4, long long some_variable5);
Notice how some_variable1 is not actually const. The referent is thought to be const, but the pointer parameter is not!
Member Variables
We are approaching the other end of the spectrum of when to use const. C++, as opposed to C, allows us to mark
individual struct member variables as const. While it sounds like a good idea, as often times we want to calculate
something and then store it in a const member variable, it complicates things. First, const member variables must
be initialized in the constructor's member initializer list. Second, having even a single const member variable
makes our object non-copyable and non-movable, which might not be what we want. If, however, we have an object that
already is non-copyable and non-movable, or that is the intention anyway, then we can benefit from const member
variables:
class Object {
public:
Object(int x, float y)
: m_x(x), m_y(y) {}
~Object();
Object(const Object&) = delete;
Object& operator=(const Object&) = delete;
Object(Object&&) = delete;
Object& operator=(Object&&) = delete;
private:
const int m_x;
const float m_y;
};
Temporary References
Temporary references, also known as rvalue references, are usually seen as mutable references, but the language
allows them to also be const. const rvalue references are very rarely used in practice. They are used by the
standard library in very select places and for very specific reasons. We won't deal with them.
class Object;
// Reference parameter needs to be mutable
void do_something(Object&& object);
Actual Constants... Again?
I've talked about marking constants with const. I kind of lied to you. While that may seem like the obvious
correct thing to do, in C++ we should actually use constexpr instead:
void update_position() {
constexpr double SPEED {2.0};
constexpr double HEIGHT {12.0};
position.y += sin(elapsed_time * SPEED) * HEIGHT;
}
However, that is still not perfect. C programmers tell us to use macros in place of constants, but there is a better way to do it without polluting the code with macros:
void update_position() {
static constexpr double SPEED {2.0};
static constexpr double HEIGHT {12.0};
position.y += sin(elapsed_time * SPEED) * HEIGHT;
}
We should almost always use either static constexpr or inline constexpr (in C++17). In the former example, what
generally happens is that the literal or constexpr values, which get compiled in the executable as data, are copied at
runtime onto the stack and then used as local constant variables, which is not optimal. In the second example,
the constants act exactly like macros, as if they were inlined into the expression as literals. Nothing gets copied
onto the stack.
The general rule is that we want to mark constexpr variables with static when they are inside functions, when they are
member variables and when they are global in a compilation unit (cpp file). We want to mark constexpr variables with
inline when they are global in headers. inline constexpr means "compile this constant data once in the binary program,
even if it is defined multiple times".
In C++, always prefer constexpr over const and macros. In C, const and macros are fine.
Function Return Values
Finally, return values should simply never be marked as const, because it doesn't actually do anything besides
sometimes making the compiler generate worse machine code.
// Non-const string
std::string get_base_file_path();
We're Stuck Using const... for Good
I mentioned in the beginning, that const doesn't exist in the machine code, but only in C++. That means that
const could be optional and we could never use it, if we didn't want to (just like parameters).
There is a joke saying that you don't ever need to pass arguments to functions, if every variable in your program is a global variable.
In machine code, const doesn't exist, but constexpr does, as literal values and other data in the executable.
The reason why we should fully embrace const in our C++ programs is that it's too late to get rid of it. If we
were to start a new C++ project while trying to never ever use const, we would fail, because everyone else
more or less already uses it and it forces us to also use it. Suppose we have a big codebase where we incorrectly
don't use const in some places. If we were to mark a parameter reference as const, then it would require some
methods to be also marked as const and that will require more references to be marked as const and so on, like
a snowball rolling down a hill. The solution is not to avoid const, but to use it correctly all the time! That
way we keep the programmers of a big codebase sane.
const was invented by some smart programmers to help us, the programmers. We should use it for our own good.
With that said, I think those smart programmers that invented const got it wrong too. I personally believe const
should have been the default and there should have been a mut keyword to mark a variable mutable instead.
Everything that I've said in this article applies to C pointers and local variables as well.
Let's write good code!