Const correctness

C++ · Practical · 12 min

Const is a contract the compiler checks; const-correct interfaces are safer, clearer, and required to work with const objects.

const says "this won't change", and the compiler holds you to it. It appears in three main places.

Const variables. const int max_items = 10; can't be modified after initialization. Use it by default for values you don't intend to change; it makes code easier to reason about. (For values known at compile time, constexpr is even stronger; see the Advanced lessons.)

Const references and pointers in parameters. void report(const Inventory& inv) promises the caller that inv won't be modified, and it accepts temporaries and const objects. For pointers, read right to left: const int* p is a pointer to a const int (can't change *p, can repoint p), while int* const p is a const pointer to a mutable int.

Const member functions. std::size_t count() const; means the function doesn't modify the object. Only const member functions can be called on a const object or through a const reference. That makes const contagious in a good way: if count() forgets its const, nobody holding a const Inventory& can call it. Mark every member function that doesn't change observable state as const.

Getters often come in a pair, a const version returning const T& and a non-const version returning T&, as std::vector::operator[] does.

Sometimes a const function needs to update something that isn't part of the object's logical state: a cache, a statistics counter or a mutex. Declare such members mutable. Use it sparingly, and remember a const function may be called from several threads at once, so mutable state needs synchronization.

Avoid const_cast to strip const; modifying an object that was defined const is undefined behaviour.

Example

#include <iostream>
#include <string>
#include <utility>
#include <vector>

class Inventory {
public:
    void add(std::string item) { items_.push_back(std::move(item)); }  // modifies: not const
    std::size_t count() const { return items_.size(); }               // read-only
    bool has(const std::string& name) const {
        ++lookups_;                                   // allowed: lookups_ is mutable
        for (const auto& i : items_) if (i == name) return true;
        return false;
    }
    int lookups() const { return lookups_; }
private:
    std::vector<std::string> items_;
    mutable int lookups_ = 0;                         // statistics, not logical state
};

void report(const Inventory& inv) {                   // promises not to modify inv
    std::cout << inv.count() << ' ' << inv.has("rope") << '\n';
    // inv.add("x");                                  // error: add() is not const
}

int main() {
    const int max_items = 10;
    Inventory inv;
    inv.add("rope");
    report(inv);                                                    // 1 1
    std::cout << max_items - inv.count() << ' ' << inv.lookups() << '\n';  // 9 1
}

Remember

  • Mark every member function that doesn't change the object const
  • const int* = pointer to const; int* const = const pointer
  • mutable is for caches and counters, not for real state

Watch out for

  • Forgetting const on a getter, which makes it uncallable through every const& in the codebase.

Check yourself

  1. `void show(const Widget& w) { w.draw(); }` fails to compile. What is the most likely fix?
  2. Given `int a = 1, b = 2; const int* p = &a;`, which statement compiles?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: en.cppreference.com/w/cpp/language/cv