Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 46

Mastering `const` Correctness

Recommended resources

Introduction

Hello! In our last lesson, we explored operator overloading, which allows us to make our custom classes more intuitive. You probably noticed the const keyword appearing frequently in our examples, both for function parameters (const MusicNote& other) and at the end of member function declarations (bool operator==(const MusicNote& other) const). We used it as a matter of good practice, but we haven't yet dived into what it truly means and why it's so fundamental to C++.

This lesson is all about mastering the const keyword. It's more than just a way to declare constants; it's a critical tool for communicating intent, enabling compiler optimizations, and writing safer, more robust code. This practice is often referred to as const-correctness.

By the end of this lesson, you will be able to correctly use the const keyword with variables, function parameters, and member functions, fully understanding the guarantees it provides.


1. The Core Idea: const as a Promise

At its heart, const is a promise you make to the compiler and to other developers. When you declare something as const, you are promising that you will not change its value. The compiler, in turn, will enforce this promise, issuing an error if you try to break it.

Let's start with a quick video that introduces this core concept.

CONST in C++

This clip from The Cherno's popular C++ series explains the fundamental idea behind const as a promise.

Watch the first 2 minutes and 15 seconds. Pay attention to how const changes a variable into a constant that cannot be modified after initialization.

As the video shows, the simplest use of const is for variables whose values should not change during the program's execution.

const int MAX_USERS = 100;
// MAX_USERS = 101; // This would cause a compilation error.

An interesting syntactic note is that the const qualifier can be placed on either side of the type for simple variables, and it means the same thing. You will see both styles in the wild.

const int a = 5; // Style 1
int const b = 10; // Style 2 (less common, but valid)

2. const in a Class Context: Member Functions and Parameters

The true power of const becomes apparent when we start using it with classes. It allows us to define clear boundaries about which functions can modify an object and which are read-only.

const Parameters

In the last lesson, we passed objects to functions using a const reference, like const MusicNote& note. Let's formalize why this is the preferred approach for passing complex objects that you don't intend to modify:

  1. Efficiency: It avoids making a full copy of the object, just like a regular reference (&).
  2. Safety: The const keyword prevents the function from accidentally modifying the original object that was passed in.

Understanding "const" in C++

This article from StudyPlan.dev clearly explains the motivation behind using const with reference parameters to establish what's known as const-correctness.

Read the section titled 'Implementing const-Correctness'. It perfectly frames the problem that const solves when using pass-by-reference.

const Member Functions

Now, what happens when you have a const object (or a const reference to one) and you want to call one of its member functions?

void printNoteDetails(const MusicNote& note) {
    // Can we call a getter function here?
    // std::cout << note.getPitch(); 
}

The compiler will stop you unless the member function itself is also marked as const. By adding const after a member function's parameter list, you promise that the function will not modify the state of the object. These are often called "inspector" or "getter" methods.

A const member function receives an implicit this pointer that is a pointer to a const object. This is how the compiler enforces the rule; you can't use a const this pointer to modify member variables.

Let's see a practical demonstration of why this is essential.

CONST in C++

Continuing with The Cherno, this part of the video is crucial. It explains what a const method is and, more importantly, demonstrates why you get a compile error if you try to call a non-const method on a const object reference.

Watch from 05:48 to 11:08. This is the most important concept in this lesson. Notice how adding const to the GetX() method resolves the compile error.

So, the rule is simple but strict: a const object can only call const member functions.

This is why it's good practice to mark any member function that doesn't change the object's member variables as const.


3. const with Pointers

Pointers add a layer of complexity because a pointer involves two separate entities: the pointer variable itself (which stores a memory address) and the data at that memory address. With const, we can make either one (or both) unchangeable.

The syntax can be confusing, so a useful rule is to read the declaration from right to left.

  1. Pointer to const data (const int* p or int const* p):

    • The data pointed to cannot be changed through this pointer.
    • The pointer itself can be changed to point to something else.
    • *p is const. p is not.
    • Use case: A function parameter that receives a pointer to data it should only read.
    int x = 10;
    const int* p = &x;
    // *p = 20; // ERROR: *p is const
    int y = 30;
    p = &y;   // OK: p is not const
    
  2. const pointer (int* const p):

    • The pointer itself cannot be changed to point to a different address. It must be initialized when declared.
    • The data it points to can be changed.
    • p is const. *p is not.
    • Use case: A class member that is a pointer to another object and should always point to that same object.
    int x = 10;
    int* const p = &x;
    *p = 20;   // OK: *p is not const
    int y = 30;
    // p = &y;    // ERROR: p is const
    
  3. const pointer to const data (const int* const p):

    • Neither the pointer nor the data it points to can be changed.
    • Both p and *p are const.

This textual breakdown provides a very clear summary.

Understanding "const" in C++

The StudyPlan.dev article provides an exceptionally clear, section-by-section breakdown of the four combinations of using const with pointers.

Read the section 'Using const with Pointers' and its four sub-sections. This is a great reference to bookmark for the future.


4. Advanced Concepts: mutable and const Overloading

Overloading on const

C++ allows you to overload a member function based on its const-ness. This means you can provide two versions of the same function: one for const objects and one for non-const objects.

A common use case is for an access function. The const version returns a const reference or pointer, while the non-const version returns a non-const reference or pointer, allowing modification.

Advanced C++: const and Functions

This video from Bo Qian provides a more formal look at const and explains how const-ness can be used to overload functions.

Watch from 05:07 to 08:03. Focus on how a const object calls the const version of printDocName, while a non-const object calls the non-const version.

The mutable Keyword

What if you have a member variable that you need to change inside a const member function? This might seem like it breaks the rules, but there's a specific keyword for this scenario: mutable.

A mutable member variable can be modified even in a const member function. This is intended for "implementation details" that don't affect the externally-visible state of the object, such as caching a computed value or for internal logging/debugging counters. This aligns with C++'s model of "conceptual constness" (the observable behavior is constant) rather than "bitwise constness" (the memory representation is constant).

This distinction between conceptual and bitwise constness is precisely the kind of "under the hood" detail that helps build a deeper understanding of the language design.

Const Correctness - C++ Tutorials

This article from Cprogramming.com dives into the theory behind const, explaining the difference between bitwise and conceptual constness and introducing the mutable keyword.

Read the sections 'Efficiency Gains? A note about Conceptual vs. Bitwise Constness' and 'Mutable Data in Const Classes'. This will give you insight into the why behind the mutable keyword.


5. Hands-On Exercise: Applying const-Correctness

Let's refactor our MusicNote class from the previous lesson to be fully const-correct.

Instructions:

  1. Start with your MusicNote class from the previous lesson's exercise.
  2. Add const getters: Create two new const member functions:
    • const char* getPitch() const;
    • int getOctave() const;
      These functions should simply return the corresponding private member variables.
  3. Update operator==: Ensure your equality operator from the last lesson is a const member function.
  4. Create a const-correct utility function: Write a free function void displayNote(const MusicNote& note);. Inside this function, use your new const getters (getPitch() and getOctave()) to print the note's details to the console.
  5. In main, create a const MusicNote object and test that you can pass it to displayNote and to std::cout (using your overloaded << operator).



#include <iostream>
#include <cstring>

// Forward declaration of the free function
class MusicNote; 
void displayNote(const MusicNote& note);

class MusicNote {
private:
    char* m_pitch;
    int m_octave;

public:
    MusicNote(const char* pitch = "C", int octave = 4) : m_octave(octave) {
        if (pitch) {
            m_pitch = new char[strlen(pitch) + 1];
            strcpy(m_pitch, pitch);
        } else {
            m_pitch = nullptr;
        }
    }

    ~MusicNote() {
        delete[] m_pitch;
    }

    friend std::ostream& operator<<(std::ostream& stream, const MusicNote& note);

    bool operator==(const MusicNote& other) const {
        if (this->m_octave != other.m_octave) {
            return false;
        }
        if (this->m_pitch == nullptr || other.m_pitch == nullptr) {
            return this->m_pitch == other.m_pitch;
        }
        return strcmp(this->m_pitch, other.m_pitch) == 0;
    }

    // --- YOUR NEW CONST GETTERS GO HERE ---

};

// Implementation of the stream operator
std::ostream& operator<<(std::ostream& stream, const MusicNote& note) {
    if (note.m_pitch) {
        stream << note.m_pitch << note.m_octave;
    } else {
        stream << "(rest)";
    }
    return stream;
}

// --- IMPLEMENTATION FOR YOUR GETTERS AND displayNote FUNCTION GOES HERE ---


int main() {
    const MusicNote middleC("C", 4);
    MusicNote gSharp("G#", 5);

    std::cout << "Displaying a const object: ";
    displayNote(middleC);
    
    std::cout << "Displaying a non-const object: ";
    displayNote(gSharp);
    
    std::cout << "Printing const object with <<: " << middleC << std::endl;

    // Test the getters
    std::cout << "Pitch of const object: " << middleC.getPitch() << std::endl;

    return 0;
}
Solution



#include <iostream>
#include <cstring>

// Forward declaration of the free function
class MusicNote; 
void displayNote(const MusicNote& note);

class MusicNote {
private:
    char* m_pitch;
    int m_octave;

public:
    MusicNote(const char* pitch = "C", int octave = 4) : m_octave(octave) {
        if (pitch) {
            m_pitch = new char[strlen(pitch) + 1];
            strcpy(m_pitch, pitch);
        } else {
            m_pitch = nullptr;
        }
    }

    ~MusicNote() {
        delete[] m_pitch;
    }

    friend std::ostream& operator<<(std::ostream& stream, const MusicNote& note);

    bool operator==(const MusicNote& other) const {
        if (this->m_octave != other.m_octave) {
            return false;
        }
        if (this->m_pitch == nullptr || other.m_pitch == nullptr) {
            return this->m_pitch == other.m_pitch;
        }
        return strcmp(this->m_pitch, other.m_pitch) == 0;
    }

    // --- NEW CONST GETTERS ---
    const char* getPitch() const {
        return m_pitch;
    }

    int getOctave() const {
        return m_octave;
    }
};

// Implementation of the stream operator
std::ostream& operator<<(std::ostream& stream, const MusicNote& note) {
    // Since this is a friend function and not a member,
    // it can't be const itself. But it takes a const reference,
    // so it must use const methods (or access members directly).
    // Let's use the new const getters for good practice.
    stream << note.getPitch() << note.getOctave();
    return stream;
}

// --- IMPLEMENTATION FOR displayNote FUNCTION ---
void displayNote(const MusicNote& note) {
    // Because 'note' is a const reference, we can ONLY call const member functions on it.
    // This line would fail to compile if getPitch() and getOctave() were not const.
    std::cout << "Note is " << note.getPitch() << note.getOctave() << std::endl;
}

int main() {
    const MusicNote middleC("C", 4);
    MusicNote gSharp("G#", 5);

    std::cout << "Displaying a const object: ";
    displayNote(middleC);
    
    std::cout << "Displaying a non-const object: ";
    displayNote(gSharp);
    
    std::cout << "Printing const object with <<: " << middleC << std::endl;

    // Test the getters
    std::cout << "Pitch of const object: " << middleC.getPitch() << std::endl;

    return 0;
}

Conclusion

You've now done a deep dive into one of the most important keywords in C++. Being disciplined about const-correctness from the start will pay huge dividends by making your code safer, easier to reason about, and more self-documenting.

Key Takeaways:

  • const is a promise that a value will not be modified, and the compiler enforces this promise.
  • Use const references (const T&) to pass objects to functions efficiently and safely.
  • Mark member functions as const (e.g., int getValue() const;) if they do not modify the object's state. This is mandatory for them to be callable on const objects.
  • When using const with pointers, remember to distinguish between a pointer-to-const (const int* p) and a const pointer (int* const p).
  • The mutable keyword provides a controlled exception, allowing a member to be changed inside a const function, which supports the idea of "conceptual constness".

Next Up

With a solid grasp of classes, constructors, destructors, operator overloading, and const-correctness, you have mastered the fundamentals of single-class object-oriented programming in C++. In the next lesson, we will explore one of the pillars of OOP: inheritance. You'll learn how to create new classes that reuse and extend the functionality of existing ones, building powerful "is-a" relationships in your code.