# const in C++

In this article I'm going to go over all the cases of const in C++ I see regularly.

# Why const?

First I want to briefly touch on why we should bother having to qualify as many variables, member functions, etc. whenever we can.

The topic of compiler optimizations have been written many times before. Please take a look at this GotW article for when const is effective and also when it may be ineffective at optimizing code. Another practical example can be found here by Matt Godbolt, specifically when talking about const in testFunc.

Besides compiler optimizations, const is also important for developers, arguably more so. When you something is qualified as const, it's effectively shrinking the set of runtime state you need to manage in your mental model of the code you're looking at. In other words, the fewer variables that are changing in a function, the more you can focus on the code that does change state.

void foo(int a, int b) {
    bool b = ...;

    T t = ...;
    U u = ...;

    ...
}

In foo, because nothing is qualified as const, you need to examine every single local variable and determine if you need to keep track of their state throughout the body of this function. However, with bar:

void bar(const int a, const int b) {
    bool b = ...;

    const T t = ...;
    U u = ...;

    ...
}

In this fictitious function, I qualified three local variables with const. Now for a future developer who needs to understand what this function does, step through in a debugger, and so on, we only need to observe the values of a, b, and t once, and focus on the runtime state of u and b only.

# When const?

I like to make it a habit to proactively use const, and while I continue working on code if I find I have to change the state of something and it makes sense to do so, I'll remove it. Due to having to write out an extra few keystrokes, I think it's more cumbersome than using a language like Rust which took the opposite direction (const by default rather than opt-in). Although it's annoying, the benefits are worth it.

# const with variables

Use const when you want to essentially mark the value of this variable immutable. When used with objects, you can only call special member functions (such as constructors, destructors) and const member functions.

const int& x = 123;
const bool b = true;
const T t;

# Lifetime extension

An interesting feature of using const references like in the first line above is you get lifetime extension: the literal value 123 can be bound to a reference because the compiler extends the lifetime of this value simply because we use const to tell the compiler we aren't changing the value. This would not compile if it was a mutable reference. In practice, don't worry about this too much.

# const with functions

Consider const in the return type when you're returning a pointer or reference that you want don't intend to modify. Don't use const for value types, like int, some T type you create, etc. as this doesn't provide any practical benefit in modern C++.

// BAD:
const int get() { ... }

// GOOD:
int get() { ... }

Consider const for parameters that you don't intend to modify. This advice also includes value types.

// BAD: `n` doesn't change, so it could be `const`.
int square(int n) {
    return n * n;
}

// GOOD: no intention to change `n`.
int square(const int n) {
    return n * n;
}

Do not add const qualifiers to value types in the parameter list of function declarations (such as those in header files), as these are redundant.

For example:

void foo(const int x); // BAD: no need for `const` here.
void foo(const int x) {
    // do something with x
}
void foo(int x); // GOOD
void foo(const int x) {
    // do something with x
}

Same applies to member functions.

But why?

# const with member functions

const can be applied to member functions like so:

class MyClass {
    int x = ...;
public:
    int get() const {
        return x;
    }
};

The advice for using const in regular functions apply here too.

Consider const when the member function doesn't change the state of the class. In the above example, since we simply read the value of x, we're not modifying the class in any way so it's appropriate to mark get as const-qualified.

The this pointer also becomes const-qualified by the compiler in the body of a const member function.

# mutable

There is one edge-case to be remindful of when it comes to const member functions. I'll illustrate with a common example:

class MyClass {
    int x = ...;
    mutable mutex mx = ...;
public:
    int get() const {
        int xValue;

        mx.lock();
        xValue = x;
        mx.unlock();

        return xValue;
    }
};

This is a simplified example where we use a mutex to safely get the value of x since there is concurrency involved. Notice that we call lock and unlock which changes the state of mx -- also changing the state of MyClass. We can use the mutable keyword to allow these calls in a const-qualified member function.

But why is this useful? The idea is if we have an interface that semantically makes sense to be const (think of this as read-only), we can use mutable as an escape hatch to keep our interface sensible. The only reason we needed to use mutable was due to an implementation detail hidden from the user (i.e. locking/unlocking the mutex). Remember, we only did this because the mutex is a side-effect of safe concurrency -- it is not part of the semantic state of our class and so it's acceptable to keep it logically separated from our interface.

# const with member variables

Member variables const-qualified are generally not useful except for view types (classes which are used as read-only containers for data -- think of std::string_view semantically).

Additionally, const-qualified members may prevent implicitly-defined special member functions such as default constructors.

For class constants, consider making them static.

# const with raw pointers

const has a few forms with raw pointers, so use the form most appropriate for your use case:

# const with smart pointers

const also shares a few forms with smart pointers, so use the form most appropriate for your use case:

# const with containers

Using const prevents reassignment of a container and also keeps immutable elements:

const std::vector v {1, 2, 3};
v = std::vector{3, 2, 1}; // ERROR

const std::vector v {1, 2, 3};
v[0] = 123; // ERROR

Some containers also propagate const because the operator[] or at member functions return const references:

struct Foo {
    void f() {}
};

const std::vector<Foo> v {Foo{}, Foo{}, Foo{}};
v[0].f(); // ERROR: f is not `const`-qualified

However, not all containers do this!

int arr[] = {1, 2, 3};
const std::span s {arr};
s[0] = 3; // OK

With std::span you'd need a span of const int to make this work.

# const with iterators

Most people are familiar with begin and end on containers to express the begin and end iterators respectively. The standard library also provides their const equivalents: cbegin and cend. These return the const-equivalent iterators into the container.

These are useful when you want to express algorithms or ranges where you don't intend to mutate container elements.

std::vector v {1, 2, 3};
const auto sum = std::accumulate(v.cbegin(), v.cend(), 0, std::plus{});
// sum == 6

# Lambda parameters

Lambdas don't work differently when it comes to parameters than with functions, but I often see confusion when they are passed to algorithms. Here's an example:

std::vector v {1, 2, 3};
const auto sum = std::accumulate(v.cbegin(), v.cend(), 0, 
    [](auto x, auto y) { return x + y; });

This function returns the same sum, but while this example compiles, x and y are not const. Since we're taking a copy of a simple type like an int, this works just fine, but these parameters should be const for the reasons in the opening section (see: "Why const?"). We would get a compiler error if we tried to take a mutable reference:

const auto sum = std::accumulate(v.cbegin(), v.cend(), 0, 
    [](auto& x, auto& y) { return x + y; }); // ERROR: int& is not `const`

So in general, match up the iterator qualification with the lambda parameters unless you're doing non-trivial work (in that case, it probably makes sense to move this out into a free/member function).

# const with ranges

Ranges with const is messy and complicated. In C++20: In Detail by Nicolai Josuttis, he goes into this topic in much more depth than I can here. Based on my own conclusion from reading his book, const is probably too dangerous to use safely with ranges until a bunch of problems get fixed in versions later than C++20, which is a shame because of the benefits of const.