14.9 - 重载下标运算符
Key Takeaway
When working with arrays, we typically use the subscript operator ([]) to index specific elements of an array:
1 |
|
COPY
However, consider the following IntList
class, which has a member variable that is an array:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
COPY
Because the m_list member variable is private, we can not access it directly from variable list. This means we have no way to directly get or set values in the m_list array. So how do we get or put elements into our list?
Without operator overloading, the typical method would be to create access functions:
1 2 3 4 5 6 7 8 9 |
|
COPY
While this works, it’s not particularly user friendly. Consider the following example:
1 2 3 4 5 6 7 |
|
COPY
Are we setting element 2 to the value 3, or element 3 to the value 2? Without seeing the definition of setItem()
, it’s simply not clear.
You could also just return the entire list and use operator[] to access the element:
1 2 3 4 5 6 7 8 |
|
COPY
While this also works, it’s syntactically odd:
1 2 3 4 5 6 7 |
|
COPY
Overloading operator[]
However, a better solution in this case is to overload the subscript operator ([]) to allow access to the elements of m_list. The subscript operator is one of the operators that must be overloaded as a member function. An overloaded operator[] function will always take one parameter: the subscript that the user places between the hard braces. In our IntList case, we expect the user to pass in an integer index, and we’ll return an integer value back as a result.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
COPY
Now, whenever we use the subscript operator ([]) on an object of our class, the compiler will return the corresponding element from the m_list member variable! This allows us to both get and set values of m_list directly:
1 2 3 4 5 |
|
COPY
This is both easy syntactically and from a comprehension standpoint. When list[2]
evaluates, the compiler first checks to see if there’s an overloaded operator[] function. If so, it passes the value inside the hard braces (in this case, 2) as an argument to the function.
Note that although you can provide a default value for the function parameter, actually using operator[] without a subscript inside is not considered a valid syntax, so there’s no point.
Tip
C++23 will add support for overloading operator[] with multiple subscripts.
Why operator[] returns a reference
Let’s take a closer look at how list[2] = 3
evaluates. Because the subscript operator has a higher precedence than the assignment operator, list[2]
evaluates first. list[2]
calls operator[], which we’ve defined to return a reference to list.m_list[2]
. Because operator[] is returning a reference, it returns the actual list.m_list[2]
array element. Our partially evaluated expression becomes list.m_list[2] = 3
, which is a straightforward integer assignment.
In lesson 9.2 -- Value categories (lvalues and rvalues), you learned that any value on the left hand side of an assignment statement must be an l-value (which is a variable that has an actual memory address). Because the result of operator[] can be used on the left hand side of an assignment (e.g. list[2] = 3
), the return value of operator[] must be an l-value. As it turns out, references are always l-values, because you can only take a reference of variables that have memory addresses. So by returning a reference, the compiler is satisfied that we are returning an l-value.
Consider what would happen if operator[] returned an integer by value instead of by reference. list[2]
would call operator[], which would return the value of list.m_list[2]. For example, if m_list[2] had the value of 6, operator[] would return the value 6. list[2] = 3
would partially evaluate to 6 = 3
, which makes no sense! If you try to do this, the C++ compiler will complain:
C:VCProjectsTest.cpp(386) : error C2106: '=' : left operand must be l-value
Dealing with const objects
In the above IntList example, operator[] is non-const, and we can use it as an l-value to change the state of non-const objects. However, what if our IntList object was const? In this case, we wouldn’t be able to call the non-const version of operator[] because that would allow us to potentially change the state of a const object.
The good news is that we can define a non-const and a const version of operator[] separately. The non-const version will be used with non-const objects, and the const version with const-objects.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
COPY
If we comment out the line clist[2] = 3
, the above program compiles and executes as expected.
Error checking
One other advantage of overloading the subscript operator is that we can make it safer than accessing arrays directly. Normally, when accessing arrays, the subscript operator does not check whether the index is valid. For example, the compiler will not complain about the following code:
1 2 |
|
COPY
However, if we know the size of our array, we can make our overloaded subscript operator check to ensure the index is within bounds:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
COPY
In the above example, we have used the assert() function (included in the cassert header) to make sure our index is valid. If the expression inside the assert evaluates to false (which means the user passed in an invalid index), the program will terminate with an error message, which is much better than the alternative (corrupting memory). This is probably the most common method of doing error checking of this sort.
Pointers to objects and overloaded operator[] don’t mix
If you try to call operator[] on a pointer to an object, C++ will assume you’re trying to index an array of objects of that type.
Consider the following example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
COPY
Because we can’t assign an integer to an IntList, this won’t compile. However, if assigning an integer was valid, this would compile and run, with undefined results.
Rule
Make sure you’re not trying to call an overloaded operator[] on a pointer to an object.
The proper syntax would be to dereference the pointer first (making sure to use parenthesis since operator[] has higher precedence than operator*), then call operator[]:
1 2 3 4 5 6 7 8 |
|
COPY
This is ugly and error prone. Better yet, don’t set pointers to your objects if you don’t have to.
The function parameter does not need to be an integer
As mentioned above, C++ passes what the user types between the hard braces as an argument to the overloaded function. In most cases, this will be an integer value. However, this is not required -- and in fact, you can define that your overloaded operator[] take a value of any type you desire. You could define your overloaded operator[] to take a double, a std::string, or whatever else you like.
As a ridiculous example, just so you can see that it works:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
COPY
As you would expect, this prints:
Hello, world!
Overloading operator[] to take a std::string parameter can be useful when writing certain kinds of classes, such as those that use words as indices.
Conclusion
The subscript operator is typically overloaded to provide direct access to individual elements from an array (or other similar structure) contained within a class. Because strings are often implemented as arrays of characters, operator[] is often implemented in string classes to allow the user to access a single character of the string.