10.2. Overloading operator[]

User-defined types that provide array-like access that allows both reading and writing often overload operator[].

One of the rules of the language is that operator[] must be implemented as a member function. Generally const and non-const versions of the overload are implemented.

struct T
{
          value_t& operator[](size_t index)       { return data[index]; }
    const value_t& operator[](size_t index) const { return data[index]; }
};

In addition to the member-only rule that the C++ language requires, there are a few best practice considerations for this operator. They mostly center around the fact that operator[]` can only accept a single value. What if you want to use this operator in a user defined type that behaves like a multi-dimensional array?

Often people attempt to overload operator[][] - but it does not exist.

In general, if you have a type with single dimension access, then it’s OK to overload operator[]. If your type has data in multiple dimensions, then prefer overloading operator() instead. For a detailed description of the why, refer to the following subsection.

10.2.1. Multi-dimension array access

To provide multidimensional array access semantics, e.g. to implement a 3D array access a[i][j][k] = x;, the overload for operator[] must return a reference to a 2D object, which has to have its own operator[] which returns a reference to a 1D object, which has to have operator[] which returns a reference to the element. To avoid this complexity, some libraries opt for overloading operator() instead.

Because operator() does not have the one parameter restriction that operator[] does, functions taking multiple parameters can be implemented directly without any excessive complicated function call chaining. And for users, the syntax is cleaner:

int main() {

  matrix a;

  // operator[] syntax
  a[i][j][k] = value;

  // operator() syntax
  a(i,j,k) = value;

}

In the following example, notice that our matrix class uses a simple one dimensional array as its backing store. So even though our class exposes a two-dimensional interface, our data is stored differently.

data_ = new T[rows * cols];

An array is simple and efficient. The class does not expose this implementation detail and if we wanted to replace the array with something else later, no matrix class users would be affected.

When using the operator() overload, only a single pair of functions is required, one function to return the value and other to return a value that can be assigned to a const.

This solution is general and scales up and down as needed, easily accommodating more or fewer dimensions.

T& matrix<T>::operator() (size_t row, size_t col);

const T& matrix<T>::operator() (size_t row, size_t col) const;

One note about the above examples. If you know the type T is a primitive type, or you have a non-templated matrix and you define your value type to be a built in type (int, double, etc), then you should return by value instead of const reference.

double& matrix::operator() (size_t row, size_t col);

const double matrix::operator() (size_t row, size_t col) const;

The non-const version should still be a reference to the value in the backing store, so that it can be modified.

If you don’t want users modifying your data at all, then only provide a const version of the operator overload.

In the interest of completeness, the following example shows one way to implement a 2D matrix class that provides an interface for my_matrix[i][j]. This example uses a vector of vectors, although other solutions are possible.

What are the main differences from the preceding implementation?

The backing store is a vector of vectors:

std::vector<std::vector<T>> data_;

As previously discussed, the operator[] takes at most a single parameter. This means we must return a vector<T>& from our operator.

std::vector<T>& operator[] (size_t row);

How does the second dimension work?

Recall that the vector class has its own operator[] overload. The final dimension with the element value is retrieved from the index into the column vector of the matrix.

A destructor is no longer needed because this version of the matrix class does not manage its own memory. All the memory management is handled by the vector class.


You have attempted of activities on this page