Overloading the subscript operator

The subscript operator ([]) allows array-style access to elements. The subscript operator must be overloaded as a member function.

Basic subscript operator overloading

Let's create a Playlist class that manages song indices:

#include <iostream>

class Playlist
{
private:
    int m_trackNumbers[10]{};

public:
    int& operator[](int index)
    {
        return m_trackNumbers[index];
    }
};

int main()
{
    Playlist favorites{};
    favorites[0] = 42;
    favorites[1] = 17;
    favorites[2] = 88;

    std::cout << "Track 1: " << favorites[0] << '\n';
    std::cout << "Track 2: " << favorites[1] << '\n';
    std::cout << "Track 3: " << favorites[2] << '\n';

    return 0;
}

The subscript operator returns a reference, allowing us to both read and modify elements using favorites[index] = value syntax.

Why return a reference?

When we write favorites[2] = 88, the expression favorites[2] evaluates first. If operator[] returned by value instead of by reference, we'd get a copy of the track number, and 88 would be assigned to that temporary copy (which would then be discarded). By returning a reference, we get the actual element, and the assignment works correctly.

Const and non-const versions

What if we have a const Playlist? We need both const and non-const versions:

#include <iostream>

class Playlist
{
private:
    int m_trackNumbers[10]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

public:
    int& operator[](int index)
    {
        return m_trackNumbers[index];
    }

    const int& operator[](int index) const
    {
        return m_trackNumbers[index];
    }
};

int main()
{
    Playlist myList{};
    myList[0] = 99;  // Uses non-const version
    std::cout << "Track: " << myList[0] << '\n';

    const Playlist defaultList{};
    // defaultList[0] = 99;  // Error: can't modify const object
    std::cout << "Default track: " << defaultList[0] << '\n';  // Uses const version

    return 0;
}

Adding bounds checking

Unlike built-in arrays, we can add safety checks to our subscript operator:

#include <iostream>
#include <cassert>
#include <cstddef>

class Playlist
{
private:
    static constexpr int m_capacity{10};
    int m_trackNumbers[m_capacity]{};

public:
    int& operator[](int index)
    {
        assert(index >= 0 && index < m_capacity);
        return m_trackNumbers[index];
    }

    const int& operator[](int index) const
    {
        assert(index >= 0 && index < m_capacity);
        return m_trackNumbers[index];
    }
};

int main()
{
    Playlist myList{};
    myList[0] = 42;
    myList[9] = 99;
    // myList[10] = 100;  // Assertion failure!

    std::cout << myList[0] << '\n';

    return 0;
}

Using non-integer indices

The subscript operator doesn't have to take an integer parameter. Here's an example using strings as keys:

#include <iostream>
#include <string>
#include <string_view>
#include <vector>

struct GradeEntry
{
    std::string subject{};
    int grade{};
};

class StudentGrades
{
private:
    std::vector<GradeEntry> m_grades{};

public:
    int& operator[](std::string_view subject)
    {
        for (auto& entry : m_grades)
        {
            if (entry.subject == subject)
                return entry.grade;
        }

        m_grades.push_back(GradeEntry{std::string{subject}, 0});
        return m_grades.back().grade;
    }
};

int main()
{
    StudentGrades student{};

    student["Math"] = 95;
    student["Science"] = 88;
    student["History"] = 92;

    std::cout << "Math: " << student["Math"] << '\n';
    std::cout << "Science: " << student["Science"] << '\n';

    return 0;
}

This program outputs:

Math: 95
Science: 88

Pointers and subscript operators don't mix

Be careful when using subscript operators with pointers:

Playlist* list{new Playlist{}};
// list[0] = 42;  // ERROR: treats list as array of Playlist objects
(*list)[0] = 42;  // Correct: dereference first, then use subscript
delete list;

Better yet, avoid raw pointers when possible and use objects directly or smart pointers.

Summary

Subscript operator ([]): Must be overloaded as a member function. Enables array-style element access for custom classes.

Return by reference: The subscript operator should return a reference (not a value) to allow both reading and modifying elements using syntax like obj[index] = value.

Const and non-const versions: Provide both T& operator[](int) for modifying elements and const T& operator[](int) const for read-only access to const objects.

Bounds checking: Unlike built-in arrays, overloaded subscript operators can include safety checks using assert() or other validation to catch invalid indices.

Non-integer indices: The subscript operator doesn't require an integer parameter - it can take any type, enabling string-based keys or other index types.

Pointers and subscript operators: Using [] on a pointer to an object applies to the pointer itself (treating it as an array), not the object. Dereference first: (*ptr)[index].

Overloading the subscript operator makes custom container classes work naturally with familiar array-like syntax, with the added benefit of being able to add bounds checking and use non-integer index types.