Multidimensional Subscript Operator in C++

This article provides an overview of the C++ subscript operator (operator[]), examines its traditional single-argument constraints, and explores the introduction of native multidimensional subscripting in recent language standards. Readers will learn how modern C++ eliminates legacy workarounds—such as proxy objects and the function call operator—by allowing operator[] to accept multiple arguments, streamlining custom container and matrix implementations.

What is the Subscript Operator?

The subscript operator, denoted by square brackets [], provides indexed access to elements within a sequence or collection. In native C and C++, it is built into raw arrays, where a[i] is defined as *(a + i).

In object-oriented C++, classes can overload operator[] to provide intuitive, array-like syntax for custom data structures, such as std::vector, std::array, and std::string. Historically, the overloaded operator was defined as a member function taking exactly one argument:

template <typename T>
class Array {
public:
    T& operator[](std::size_t index);
    const T& operator[](std::size_t index) const;
};

The Historical Limitation: Single-Argument Constraint

Prior to C++23, operator[] was restricted by the language grammar to accept only a single parameter. If a developer attempted to pass multiple values separated by commas, such as matrix[x, y], the compiler interpreted the comma as the built-in comma operator. This meant the expression x, y evaluated x, discarded it, and passed only y to the operator.

To implement multidimensional indexing (such as for a 2D matrix or a 3D tensor), developers had to rely on two primary workarounds:

  1. The Call Operator: Overloading operator() instead, allowing syntax like matrix(x, y). While functional, it lacked the idiomatic bracket syntax associated with indexing.
  2. Proxy Objects: Overloading operator[] to return an intermediate proxy class that also overloaded operator[], enabling chained access like matrix[x][y]. This approach added boilerplate and could hinder compiler optimizations if not inlined properly.

C++20 paved the way for a solution by officially deprecating the use of the comma operator inside subscript expressions.

Multidimensional Subscripting in C++23

Adopted through proposal P2128R6, C++23 expanded operator[] to accept zero or more arguments, including variadic parameter packs. This change aligns the subscript operator's signature capabilities with the function call operator (operator()).

Syntax and Implementation

Defining a multidimensional subscript operator requires declaring operator[] with multiple parameters:

#include <vector>
#include <cstddef>

template <typename T>
class Matrix {
private:
    std::vector<T> data;
    std::size_t cols;

public:
    Matrix(std::size_t rows, std::size_t cols) 
        : data(rows * cols), cols(cols) {}

    // C++23 Multidimensional Subscript Operator
    T& operator[](std::size_t row, std::size_t col) {
        return data[row * cols + col];
    }

    const T& operator[](std::size_t row, std::size_t col) const {
        return data[row * cols + col];
    }
};

Usage

With C++23 support enabled, callers can use comma-separated indexes directly within the square brackets:

int main() {
    Matrix<int> mat(4, 4);

    // Direct multidimensional access
    mat[1, 2] = 42;
    int value = mat[1, 2];

    return 0;
}

Variadic Subscripting

The feature also supports variadic templates, enabling arbitrary N-dimensional containers:

template <typename T, std::size_t Dimensions>
class Tensor {
public:
    template <typename... Indices>
        requires (sizeof...(Indices) == Dimensions)
    T& operator[](Indices... indices) {
        // Compute flat index and return element
    }
};

Practical Implications

The introduction of multidimensional subscripting simplifies mathematical, linear algebra, and data science libraries. It directly complements modern features such as std::mdspan (introduced in C++23), a non-owning multidimensional array view that relies on native multi-argument indexing to provide zero-overhead abstractions for contiguous and non-contiguous memory layouts.