Objects that stand for other objects are often called proxy objects (or surrogates), and the classes that give rise to proxy objects are often called proxy classes, which is useful for implementing multidimensional arrays, differentiating lvalue/rvalue, and suppressing implicit conversions.
Implementing Two-Dimensional Arrays
Consider this statement:
|
|
We want to create a general 2D array supporting operations such as data[3][6]
. However, there’s no such thing as a operator[][]
in C++. The reason it is legal to write code above that appears to use operator[][]
is because the variable data
is not really a two-dimensinal array at all, but a 10-element one-dimensional array, each element of which is itself a 20-element array. So the expression data[3][6]
really means (data[3])[6]
- the seventh element of the array that is the fourth element of data
.
Playing the same trick as above, we can define our Array2D
class by overloading operator[]
to return an object of a new class, Array1D
:
|
|
Then it is legal to write code like this:
|
|
Conceptually intances of Array1D
class (which is a proxy class) do not exist for clients of Array2D
. Such clients program as if they were using real, live two-dimensional arrays.
Distinguishing Reads from Writes via operator[]
operator[]
can be called in two different contexts:
- rvalue usage for read
- lvalue usage for write
In general, using an object as an lvalue means using it such that it might be modified, and using it as rvalue means using it such that it cannot be modified.
From MECpp item 29 reference counting, we can see reads can be much less expensive than writes - writes of reference-counted object may involve copying an entire data structure, while reads never require more than the simple returning of a value - so it will save a lot to differentiate lvalue usage from rvalue usage. However, it is impossible to tell whether operator[]
is beeing invoked in an lvalue or an rvalue context from within operator[]
- operator[]
alone does not have the ability to determine the calling context.
The solution: we delay our lvalue-vs-rvalue actions until we see how the result of operator[]
is used - by using proxy class to postpone our decision until after operator[]
has returned (lazy evaluation, see MECpp item 7):
|
|
Now let’s see how it works. Given reference-counted stirngs using proxies above String s1, s2;
,
For rvalue usage
Consider this statement cout << s1[5];
: s1[5]
yields a CharProxy
object, and compiler implicitly converts this CharProxy
into char
using the conversion operator declared in the CharProxy
class. This is representitive of the CharProxy-to-char conversion that takes place for all CharProxy
objects used as rvalues.
For lvalue usage
Lvalue usage is handled differently:
Say, for statement s2[5] = 'x';
, the expression s2[5]
yields a CharProxy
object, which is the target of an assignment, so the assignment operator in the CharProxy
class will be called - this is the crucial postponed step to differentiate writes from reads. Inside this CharProxy
assignment operator, we know the string character for which the proxy stands is being used as an lvalue.
Similarly, the statement s1[3] = s2[7];
calls the assignment operator for two CharProxy
objects, and inside the operator, we know the object on the left is being used as an lvalue and the object on the right as an rvalue.
Now that we know exactly the context in which caller invokes the operator[]
, it is easy to implement them:
|
|
|
|
Preventing implicit conversions in single-argument constructor
Refer to MECpp item 5.
Limitations
-
Taking the address
In general, taking the address of a proxy yields a different type of pointer than does taking the address of a real object. Thus, the statement
char *p = &s1[1];
will cause error. To eliminate the problem, we’ll have to overload the address-of operators forCharProxy
class:1 2 3 4 5 6 7 8 9 10
class String { public: ... class CharProxy { char * operator&(); const char * operator&() const; ... }; ... };
1 2 3 4 5 6 7 8 9 10 11 12 13
const char * String::CharProxy::operator&() { return &(theString.value->data[charIndex]); } char * String::CharProxy::operator&() { if (theString.value->isShared()) { theString.value = new StringValue(theString.value->data); } theString.value->markUnshareable(); return &(theString.value->data[charIndex]); }
-
Integrating with templates
If we have a template for reference-counted arrays that use proxy classes to distringuish lvalue and rvalue invocations of operator[]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
template<class T> class Array { public: class Proxy { public: Proxy(Array<T>& array, int index); Proxy& operator=(const T& rhs); operator T() const; ... }; const Proxy operator[](int index) const; Proxy operator[](int index); ... };
Then for
Array<int> intArray;
, we can’t make statement such asintArray[5] += 5;
or++intArray[5];
, sinceoperator+=
andoperator++
is not defined for proxy objects. To solve this problem, we have to define each of these functions for theArray<T>::Proxy
, which, unfortunately, is a lot of work.Similarly, we can’t invoke member functions on real objects through proxies. For an array taking
Rational
as elements (Array<Rational> array;
), there is no way to invokeRational
’s member function like this:1 2
cout << array[4].numerator(); // error! int denom = array[22].denominator(); // error!
The solution is similar: we need to overload these functions so that they also apply to proxies.
-
Passed to functions taking references to non-const objects
1 2 3
void swap(char& a, char& b); String s = "+C+"; swap(s[0], s[1]); // won't compile
A
CharProxy
may be implicitly converted into achar
, but there is no conversion function to achar&
. Further more, thechar
to which it may be converted can’t be bound to swap’schar&
parameters, because thatchar
is a temporary object (operator char
returns by value,) and, as MECpp item 19 explains, temporary objects are refused to be bound to non-const reference parameters. -
Implicit type conversions
The process where a proxy object implicitly converted into the real object it stands for, a user-defined conversion function is invoked. As MECpp item 5 explains, only one user-defined conversion function is used by compiler when implicitly converting a parameter at a call site into the type needed by the corresponding function parameter.