Using the subscript operator to access elements in a collection
Accessing elements of an array is a basic feature not just in C++ but also in any programming language that supports arrays. The syntax is also the same across many programming languages. In C++, the subscript operator used for this purpose, []
, can be overloaded to provide access to data in a class. Typically, this is the case for classes that model containers. In this recipe, we’ll see how to leverage this operator and what changes C++23 brings.
How to do it…
To provide random access to elements in a container, overload the subscript operator as follows:
- For one-dimensional containers, you can overload the subscript operator with one argument, regardless of the version of the standard:
template <typename T> struct some_buffer { some_buffer(size_t const size):data(size) {} size_t size() const { return data.size(); } T const& operator[](size_t const index) const { if(index >= data.size()) std::runtime_error("invalid index"); return data[index]; } T & operator[](size_t const index) { if (index >= data.size()) std::runtime_error("invalid index"); return data[index]; } private: std::vector<T> data; };
- For multidimensional containers, in C++23, you can overload the subscript operator with multiple arguments:
template <typename T, size_t ROWS, size_t COLS> struct matrix { T& operator[](size_t const row, size_t const col) { if(row >= ROWS || col >= COLS) throw std::runtime_error("invalid index"); return data[row * COLS + col]; } T const & operator[](size_t const row, size_t const col) const { if (row >= ROWS || col >= COLS) throw std::runtime_error("invalid index"); return data[row * COLS + col]; } private: std::array<T, ROWS* COLS> data; };
How it works…
The subscript operator is used to access elements in an array. However, it is possible to overload it as a member function in classes typically modeling containers (or collections in general) to access its elements. Standard containers such as std::vector
, std::set
, and std::map
provide overloads for the subscript operator for this purpose. Therefore, you can write code as follows:
std::vector<int> v {1, 2, 3};
v[2] = v[1] + v[0];
In the previous section, we saw how the subscript operator can be overloaded. There are typically two overloads, one that is constant and one that is mutable. The const-qualified overload returns a reference to a constant object, while the mutable overload returns a reference.
The major problem with the subscript operator was that, prior to C++23, it could only have one parameter. Therefore, it could not be used to provide access to elements of a multidimensional container. As a result, developers usually resorted to using the call operator for this purpose. An example is shown in the following snippet:
template <typename T, size_t ROWS, size_t COLS>
struct matrix
{
T& operator()(size_t const row, size_t const col)
{
if(row >= ROWS || col >= COLS)
throw std::runtime_error("invalid index");
return data[row * COLS + col];
}
T const & operator()(size_t const row, size_t const col) const
{
if (row >= ROWS || col >= COLS)
throw std::runtime_error("invalid index");
return data[row * COLS + col];
}
private:
std::array<T, ROWS* COLS> data;
};
matrix<int, 2, 3> m;
m(0, 0) = 1;
To help with this, and allow a more consistent approach, C++11 made it possible to use the subscript operator with the syntax [{expr1, expr2, …}]
. A modified implementation of the matrix
class that leverages this syntax is shown next:
template <typename T, size_t ROWS, size_t COLS>
struct matrix
{
T& operator[](std::initializer_list<size_t> index)
{
size_t row = *index.begin();
size_t col = *(index.begin() + 1);
if (row >= ROWS || col >= COLS)
throw std::runtime_error("invalid index");
return data[row * COLS + col];
}
T const & operator[](std::initializer_list<size_t> index) const
{
size_t row = *index.begin();
size_t col = *(index.begin() + 1);
if (row >= ROWS || col >= COLS)
throw std::runtime_error("invalid index");
return data[row * COLS + col];
}
private:
std::array<T, ROWS* COLS> data;
};
matrix<int, 2, 3> m;
m[{0, 0}] = 1;
However, the syntax is rather cumbersome and was probably rarely used in practice. For this reason, the C++23 standard makes it possible to overload the subscript operator using multiple parameters. A modified matrix
class is shown here:
template <typename T, size_t ROWS, size_t COLS>
struct matrix
{
T& operator[](size_t const row, size_t const col)
{
if(row >= ROWS || col >= COLS)
throw std::runtime_error("invalid index");
return data[row * COLS + col];
}
T const & operator[](size_t const row, size_t const col) const
{
if (row >= ROWS || col >= COLS)
throw std::runtime_error("invalid index");
return data[row * COLS + col];
}
private:
std::array<T, ROWS* COLS> data;
};
matrix<int, 2, 3> m;
m[0, 0] = 1;
This makes the calling syntax consistent with accessing one-dimensional containers. This is used by std::mdspan
to provide element access. This is a new C++23 class that represents a non-owning view into a contiguous sequence (such as an array), but it reinterprets the sequence as a multidimensional array.
The matrix
class shown previously can actually be replaced with an mdspan
view over an array, as shown in the following snippet:
int data[2*3] = {};
auto m = std::mdspan<int, std::extents<2, 3>> (data);
m[0, 0] = 1;
See also
- Chapter 5, Writing your own random-access iterator, to see how you can write an iterator for accessing the elements of a container
- Chapter 6, Using std::mdspan for multidimensional views of sequences of objects, to learn more about the
std::mdspan
class
Learn more on Discord
Join our community’s Discord space for discussions with the author and other readers:
https://discord.gg/7xRaTCeEhx