11.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.
11.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.
operator() Example
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.
Run It
1#include <algorithm>
2#include <cstdlib>
3#include <iostream>
4#include <stdexcept>
5
6template <class T>
7class matrix {
8public:
9 matrix(size_t rows, size_t cols);
10 T& operator() (size_t row, size_t col);
11 const T& operator() (size_t row, size_t col) const;
12 // ...
13 ~matrix();
14 explicit matrix(const matrix& m);
15 // matrix& operator= (const matrix& m);
16 // many other useful functions not implemented
17private:
18 size_t rows_;
19 size_t cols_;
20 T* data_;
21};
22
23template <class T>
24inline
25matrix<T>::matrix(size_t rows, size_t cols)
26 : rows_ (rows)
27 , cols_ (cols)
28{
29 if (rows == 0 || cols == 0)
30 throw std::out_of_range("Matrix constructor has 0 size");
31 data_ = new T[rows * cols];
32 for (size_t i = 0; i < rows_*cols_; ++i) {
33 data_[i] = 0;
34 }
35}
36
37template <class T>
38inline
39matrix<T>::matrix(const matrix<T>& m)
40 : rows_ (m.rows_)
41 , cols_ (m.cols_)
42{
43 std::copy(m.data_, m.data_+rows_*cols_, data_);
44}
45
46template <class T>
47inline
48matrix<T>::~matrix()
49{
50 delete[] data_;
51}
52
53template <class T>
54inline
55T& matrix<T>::operator() (size_t row, size_t col)
56{
57 if (row >= rows_ || col >= cols_)
58 throw std::out_of_range("Matrix subscript out of bounds");
59 return data_[cols_*row + col];
60}
61
62template <class T>
63inline
64const T& matrix<T>::operator() (size_t row, size_t col) const
65{
66 if (row >= rows_ || col >= cols_)
67 throw std::out_of_range("const Matrix subscript out of bounds");
68 return data_[cols_*row + col];
69}
70
71int main()
72{
73 matrix<double>a {3,5};
74 a(0,0) = -1;
75 a(1,1) = 1;
76 a(1,2) = 2;
77 a(1,3) = 3;
78 a(2,4) = 5;
79
80 for (size_t i = 0; i<3; ++i) {
81 for (size_t j = 0; j<5; ++j) {
82 std::cout << a(i,j) << ' ';
83 }
84 std::cout << '\n';
85 }
86}
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.
operator[] Example
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.
Run It
1#include <cstdlib>
2#include <iostream>
3#include <vector>
4
5template <class T>
6class matrix {
7 public:
8 matrix(size_t rows, size_t cols);
9 explicit matrix(const matrix& m);
10
11 std::vector<T>& operator[] (size_t row);
12 const std::vector<T>& operator[] (size_t row) const;
13
14 size_t rows() const {return data_.size(); }
15 size_t cols() const
16 {
17 return rows()? data_[0].size(): 0;
18 }
19 private:
20 std::vector<std::vector<T>> data_;
21};
22
23template <class T>
24inline
25matrix<T>::matrix(size_t rows, size_t cols)
26 : data_ (rows)
27{
28 for (auto& row: data_) {
29 row.resize(cols);
30 }
31}
32
33template <class T>
34inline
35matrix<T>::matrix(const matrix<T>& m)
36 : data_ (m.data_)
37{ }
38
39template <class T>
40inline
41std::vector<T>& matrix<T>::operator[] (size_t row)
42{
43 return data_[row];
44}
45
46template <class T>
47inline
48const std::vector<T>& matrix<T>::operator[] (size_t row) const
49{
50 return data_[row];
51}
52
53int main()
54{
55 matrix<double>a {3,5};
56 a[0][0] = -1;
57 a[1][1] = 1;
58 a[1][2] = 2;
59 a[1][3] = 3;
60 a[2][4] = 5;
61
62 for (size_t i = 0; i<3; ++i) {
63 for (size_t j = 0; j<5; ++j) {
64 std::cout << a[i][j] << ' ';
65 }
66 std::cout << '\n';
67 }
68}
More to Explore