More Effective C++ | Item 30: Proxy classes Back to Item 29: Reference counting Continue to Item 31: Making functions virtual with respect to more than one object Item 30: Proxy classes. Though your in-laws may be one-dimensional, the world, in general, is not. Unfortunately, C++ hasn't yet caught on to that fact. At least, there's little evidence for it in the language's support for arrays. You can create two-dimensional, three-dimensional heck, you can create n-dimensional arrays in FORTRAN, in BASIC, even in COBOL (okay, FORTRAN only allows up to seven dimensions, but let's not quibble), but can you do it in C++? Only sometimes, and even then only sort of. This much is legal: int data[10][20]; // 2D array: 10 by 20 The corresponding construct using variables as dimension sizes, however, is not: void processInput(int dim1, int dim2) { int data[dim1][dim2]; // error! array dimensions ... // must be known during } // compilation It's not even legal for a heap-based allocation: int *data = new int[dim1][dim2]; // error! Implementing Two-Dimensional Arrays Multidimensional arrays are as useful in C++ as they are in any other language, so it's important to come up with a way to get decent support for them. The usual way is the standard one in C++: create a class to represent the objects we need but that are missing in the language proper. Hence we can define a class template for two-dimensional arrays: template class Array2D { public: Array2D(int dim1, int dim2); ... }; Now we can define the arrays we want: Array2D data(10, 20); // fine Array2D *data = new Array2D(10, 20); // fine void processInput(int dim1, int dim2) { Array2D data(dim1, dim2); // fine ... } Using these array objects, however, isn't quite as straightforward. In keeping with the grand syntactic tradition of both C and C++, we'd like to be able to use brackets to index into our arrays, cout << data[3][6]; but how do we declare the indexing operator in Array2D to let us do this? Our first impulse might be to declare operator[][] functions, like this: template class Array2D { public: // declarations that won't compile T& operator[][](int index1, int index2); const T& operator[][](int index1, int index2) const; ... }; We'd quickly learn to rein in such impulses, however, because there is no such thing as operator[][], and don't think your compilers will forget it. (For a complete list of operators, overloadable and otherwise, see Item 7.) We'll have to do something else. If you can stomach the syntax, you might follow the lead of the many programming languages that use parentheses to index into arrays. To use parentheses, you just overload operator(): template class Array2D { public: // declarations that will compile T& operator()(int index1, int index2); const T& operator()(int index1, int index2) const; ... }; Clients then use arrays this way: cout << data(3, 6); This is easy to implement and easy to generalize to as many dimensions as you like. The drawback is that your Array2D objects don't look like built-in arrays any more. In fact, the above access to element (3, 6) of data looks, on the face of it, like a function call. If you reject the thought of your arrays looking like FORTRAN refugees, you might turn again to the notion of using brackets as the indexing operator. Although there is no such thing as operator[][], it is nonetheless legal to write code that appears to use it: int data[10][20]; ... cout << data[3][6]; // fine What gives? What gives is that the variable data is not really a two-dimensional array at all, it's a 10-element one-dimensional array. Each of those 10 elements is itself a 20-element array, so the expression data[3][6] really means (data[3])[6], i.e., the seventh element of the array that is the fourth element of data. In short, the value yielded by the first application of the brackets is another array, so the second application of the brackets gets an element from that secondary array. We can play the same game with our Array2D class by overloading operator[] to return an object of a new class, Array1D. We can then overload operator[] again in Array1D to return an element in our original two-dimensional array: template class Array2D { public: class Array1D { public: T& operator[](int index); const T& operator[](int index) const; ... }; Array1D operator[](int index); const Array1D operator[](int index) const; ... }; The following then becomes legal: Array2D data(10, 20); ... cout << data[3][6]; // fine Here, data[3] yields an Array1D object and the operator[] invocation on that object yields the float in position (3, 6) of the original two-dimensional array. Clients of the Array2D class need not be aware of the presence of the Array1D class. Objects of this latter class stand for one-dimensional array objects that, conceptually, do not exist for clients of Array2D. Such clients program as if they were using real, live, honest-to-Allah two-dimensional arrays. It is of no concern to Array2D clients that those objects must, in order to satisfy the vagaries of C++, be syntactically compatible with one-dimensional arrays of other one-dimensional arrays. Each Array1D object stands for a one-dimensional array that is absent from the conceptual model used by clients of Array2D. Objects that stand for other objects are often called proxy objects, and the classes that give rise to proxy objects are often called proxy classes. In this example, Array1D is a proxy class. Its instances stand for one-dimensional arrays that, conceptually, do not exist. (The terminology for proxy objects and classes is far from universal; objects of such classes are also sometimes known as surrogates.) Distinguishing Reads from Writes via operator[] The use of proxies to implement classes whose instances act like multidimensional arrays is common, but proxy classes are more flexible than that. Item 5, for example, shows how proxy classes can be employed to prevent single-argument constructors from being used to perform unwanted type conversions. Of the varied uses of proxy classes, however, the most heralded is that of helping distinguish reads from writes through operator[]. Consider a reference-counted string type that supports operator[]. Such a type is examined in detail in Item 29. If the concepts behind reference counting have slipped your mind, it would be a good idea to familiarize yourself with the material in that Item now. A string type supporting operator[] allows clients to write code like this: String s1, s2; // a string-like class; the // use of proxies keeps this // class from conforming to // the standard string ... // interface cout << s1[5]; // read s1 s2[5] = 'x'; // write s2 s1[3] = s2[8]; // write s1, read s2 Note that operator[] can be called in two different contexts: to read a character or to write a character. Reads are known as rvalue usages; writes are known as lvalue usages. (The terms come from the field of compilers, where an lvalue goes on the left-hand side of an assignment and an rvalue goes on the right-hand side.) In general, using an object as an lvalue means using it such that it might be modified, and using it as an rvalue means using it such that it cannot be modified. We'd like to distinguish between lvalue and rvalue usage of operator[] because, especially for reference-counted data structures, reads can be much less expensive to implement than writes. As Item 29 explains, writes of reference-counted objects may involve copying an entire data structure, but reads never require more than the simple returning of a value. Unfortunately, inside operator[], there is no way to determine the context in which the function was called; it is not possible to distinguish lvalue usage from rvalue usage within operator[]. "But wait," you say, "we don't need to. We can overload operator[] on the basis of its constness, and that will allow us to distinguish reads from writes." In other words, you suggest we solve our problem this way: class String { public: const char& operator[](int index) const; // for reads char& operator[](int index); // for writes ... }; Alas, this won't work. Compilers choose between const and non-const member functions by looking only at whether the object invoking a function is const. No consideration is given to the context in which a call is made. Hence: String s1, s2; ... cout << s1[5]; // calls non-const operator[], // because s1 isn't const s2[5] = 'x'; // also calls non-const // operator[]: s2 isn't const s1[3] = s2[8]; // both calls are to non-const // operator[], because both s1 // and s2 are non-const objects Overloading operator[], then, fails to distinguish reads from writes. In Item 29, we resigned ourselves to this unsatisfactory state of affairs and made the conservative assumption that all calls to operator[] were for writes. This time we shall not give up so easily. It may be impossible to distinguish lvalue from rvalue usage inside operator[], but we still want to do it. We will therefore find a way. What fun is life if you allow yourself to be limited by the possible? Our approach is based on the fact that though it may be impossible to tell whether operator[] is being invoked in an lvalue or an rvalue context from within operator[], we can still treat reads differently from writes if we delay our lvalue-versus-rvalue actions until we see how the result of operator[] is used. All we need is a way to postpone our decision on whether our object is being read or written until after operator[] has returned. (This is an example of lazy evaluation see Item 17.) A proxy class allows us to buy the time we need, because we can modify operator[] to return a proxy for a string character instead of a string character itself. We can then wait to see how the proxy is used. If it's read, we can belatedly treat the call to operator[] as a read. If it's written, we must treat the call to operator[] as a write. We will see the code for this in a moment, but first it is important to understand the proxies we'll be using. There are only three things you can do with a proxy: Create it, i.e., specify which string character it stands for. Use it as the target of an assignment, in which case you are really making an assignment to the string character it stands for. When used in this way, a proxy represents an lvalue use of the string on which operator[] was invoked. Use it in any other way. When used like this, a proxy represents an rvalue use of the string on which operator[] was invoked. Here are the class definitions for a reference-counted String class using a proxy class to distinguish between lvalue and rvalue usages of operator[]: class String { // reference-counted strings; public: // see Item 29 for details class CharProxy { // proxies for string chars public: CharProxy(String& str, int index); // creation CharProxy& operator=(const CharProxy& rhs); // lvalue CharProxy& operator=(char c); // uses operator char() const; // rvalue // use private: String& theString; // string this proxy pertains to int charIndex; // char within that string // this proxy stands for }; // continuation of String class const CharProxy operator[](int index) const; // for const Strings CharProxy operator[](int index); // for non-const Strings ... friend class CharProxy; private: RCPtr value; }; Other than the addition of the CharProxy class (which we'll examine below), the only difference between this String class and the final String class in Item 29 is that both operator[] functions now return CharProxy objects. Clients of String can generally ignore this, however, and program as if the operator[] functions returned characters (or references to characters see Item 1) in the usual manner: String s1, s2; // reference-counted strings // using proxies ... cout << s1[5]; // still legal, still works s2[5] = 'x'; // also legal, also works s1[3] = s2[8]; // of course it's legal, // of course it works What's interesting is not that this works. What's interesting is how it works. Consider first this statement: cout << s1[5]; The expression s1[5] yields a CharProxy object. No output operator is defined for such objects, so your compilers labor to find an implicit type conversion they can apply to make the call to operator<< succeed (see Item 5). They find one: the implicit conversion from CharProxy to char declared in the CharProxy class. They automatically invoke this conversion operator, and the result is that the string character represented by the CharProxy is printed. This is representative of the CharProxy-to-char conversion that takes place for all CharProxy objects used as rvalues. Lvalue usage is handled differently. Look again at s2[5] = 'x'; As before, the expression s2[5] yields a CharProxy object, but this time that object is the target of an assignment. Which assignment operator is invoked? The target of the assignment is a CharProxy, so the assignment operator that's called is in the CharProxy class. This is crucial, because inside a CharProxy assignment operator, we know that the CharProxy object being assigned to is being used as an lvalue. We therefore know that the string character for which the proxy stands is being used as an lvalue, and we must take whatever actions are necessary to implement lvalue access for that character. Similarly, the statement s1[3] = s2[8]; calls the assignment operator for two CharProxy objects, and inside that operator we know the object on the left is being used as an lvalue and the object on the right as an rvalue. "Yeah, yeah, yeah," you grumble, "show me." Okay. Here's the code for String's operator[] functions: const String::CharProxy String::operator[](int index) const { return CharProxy(const_cast(*this), index); } String::CharProxy String::operator[](int index) { return CharProxy(*this, index); } Each function just creates and returns a proxy for the requested character. No action is taken on the character itself: we defer such action until we know whether the access is for a read or a write. Note that the const version of operator[] returns a const proxy. Because CharProxy::operator= isn't a const member function, such proxies can't be used as the target of assignments. Hence neither the proxy returned from the const version of operator[] nor the character for which it stands may be used as an lvalue. Conveniently enough, that's exactly the behavior we want for the const version of operator[]. Note also the use of a const_cast (see Item 2) on *this when creating the CharProxy object that the const operator[] returns. That's necessary to satisfy the constraints of the CharProxy constructor, which accepts only a non-const String. Casts are usually worrisome, but in this case the CharProxy object returned by operator[] is itself const, so there is no risk the String containing the character to which the proxy refers will be modified. Each proxy returned by an operator[] function remembers which string it pertains to and, within that string, the index of the character it represents: String::CharProxy::CharProxy(String& str, int index) : theString(str), charIndex(index) {} Conversion of a proxy to an rvalue is straightforward we just return a copy of the character represented by the proxy: String::CharProxy::operator char() const { return theString.value->data[charIndex]; } If you've forgotten the relationship among a String object, its value member, and the data member it points to, you can refresh your memory by turning to Item 29. Because this function returns a character by value, and because C++ limits the use of such by-value returns to rvalue contexts only, this conversion function can be used only in places where an rvalue is legal. We thus turn to implementation of CharProxy's assignment operators, which is where we must deal with the fact that a character represented by a proxy is being used as the target of an assignment, i.e., as an lvalue. We can implement CharProxy's conventional assignment operator as follows: String::CharProxy& String::CharProxy::operator=(const CharProxy& rhs) { // if the string is sharing a value with other String objects, // break off a separate copy of the value for this string only if (theString.value->isShared()) { theString.value = new StringValue(theString.value->data); } // now make the assignment: assign the value of the char // represented by rhs to the char represented by *this theString.value->data[charIndex] = rhs.theString.value->data[rhs.charIndex]; return *this; } If you compare this with the implementation of the non-const String::operator in Item 29, you'll see that they are strikingly similar. This is to be expected. In Item 29, we pessimistically assumed that all invocations of the non-const operator[] were writes, so we treated them as such. Here, we moved the code implementing a write into CharProxy's assignment operators, and that allows us to avoid paying for a write when the non-const operator[] is used only in an rvalue context. Note, by the way, that this function requires access to String's private data member value. That's why CharProxy is declared a friend in the earlier class definition for String. The second CharProxy assignment operator is almost identical: String::CharProxy& String::CharProxy::operator=(char c) { if (theString.value->isShared()) { theString.value = new StringValue(theString.value->data); } theString.value->data[charIndex] = c; return *this; } As an accomplished software engineer, you would, of course, banish the code duplication present in these two assignment operators to a private CharProxy member function that both would call. Aren't you the modular one? Limitations The use of a proxy class is a nice way to distinguish lvalue and rvalue usage of operator[], but the technique is not without its drawbacks. We'd like proxy objects to seamlessly replace the objects they stand for, but this ideal is difficult to achieve. That's because objects are used as lvalues in contexts other than just assignment, and using proxies in such contexts usually yields behavior different from using real objects. Consider again the code fragment from Item 29 that motivated our decision to add a shareability flag to each StringValue object. If String::operator[] returns a CharProxy instead of a char&, that code will no longer compile: String s1 = "Hello"; char *p = &s1[1]; // error! The expression s1[1] returns a CharProxy, so the type of the expression on the right-hand side of the "=" is CharProxy*. There is no conversion from a CharProxy* to a char*, so the initialization of p fails to compile. In general, taking the address of a proxy yields a different type of pointer than does taking the address of a real object. To eliminate this difficulty, you'll need to overload the address-of operators for the CharProxy class: class String { public: class CharProxy { public: ... char * operator&(); const char * operator&() const; ... }; ... }; These functions are easy to implement. The const function just returns a pointer to a const version of the character represented by the proxy: const char * String::CharProxy::operator&() const { return &(theString.value->data[charIndex]); } The non-const function is a bit more work, because it returns a pointer to a character that may be modified. This is analogous to the behavior of the non-const version of String::operator[] in Item 29, and the implementation is equally analogous: char * String::CharProxy::operator&() { // make sure the character to which this function returns // a pointer isn't shared by any other String objects if (theString.value->isShared()) { theString.value = new StringValue(theString.value->data); } // we don't know how long the pointer this function // returns will be kept by clients, so the StringValue // object can never be shared theString.value->markUnshareable(); return &(theString.value->data[charIndex]); } Much of this code is common to other CharProxy member functions, so I know you'd encapsulate it in a private member function that all would call. A second difference between chars and the CharProxys that stand for them becomes apparent if we have a template for reference-counted arrays that use proxy classes to distinguish lvalue and rvalue invocations of operator[]: template // reference-counted array class Array { // using proxies public: class Proxy { public: Proxy(Array& array, int index); Proxy& operator=(const T& rhs); operator T() const; ... }; const Proxy operator[](int index) const; Proxy operator[](int index); ... }; Consider how these arrays might be used: Array intArray; ... intArray[5] = 22; // fine intArray[5] += 5; // error! ++intArray[5]; // error! As expected, use of operator[] as the target of a simple assignment succeeds, but use of operator[] on the left-hand side of a call to operator+= or operator++ fails. That's because operator[] returns a proxy, and there is no operator+= or operator++ for Proxy objects. A similar situation exists for other operators that require lvalues, including operator*=, operator<<=, operator--, etc. If you want these operators to work with operator[] functions that return proxies, you must define each of these functions for the Array::Proxy class. That's a lot of work, and you probably don't want to do it. Unfortunately, you either do the work or you do without. Them's the breaks. A related problem has to do with invoking member functions on real objects through proxies. To be blunt about it, you can't. For example, suppose we'd like to work with reference-counted arrays of rational numbers. We could define a class Rational and then use the Array template we just saw: class Rational { public: Rational(int numerator = 0, int denominator = 1); int numerator() const; int denominator() const; ... }; Array array; This is how we'd expect to be able to use such arrays, but, alas, we'd be disappointed: cout << array[4].numerator(); // error! int denom = array[22].denominator(); // error! By now the difficulty is predictable; operator[] returns a proxy for a rational number, not an actual Rational object. But the numerator and denominator member functions exist only for Rationals, not their proxies. Hence the complaints by your compilers. To make proxies behave like the objects they stand for, you must overload each function applicable to the real objects so it applies to proxies, too. Yet another situation in which proxies fail to replace real objects is when being passed to functions that take references to non-const objects: void swap(char& a, char& b); // swaps the value of a and b String s = "+C+"; // oops, should be "C++" swap(s[0], s[1]); // this should fix the // problem, but it won't // compile String::operator[] returns a CharProxy, but swap demands that its arguments be of type char&. A CharProxy may be implicitly converted into a char, but there is no conversion function to a char&. Furthermore, the char to which it may be converted can't be bound to swap's char& parameters, because that char is a temporary object (it's operator char's return value) and, as Item 19 explains, there are good reasons for refusing to bind temporary objects to non-const reference parameters. A final way in which proxies fail to seamlessly replace real objects has to do with implicit type conversions. When a proxy object is implicitly converted into the real object it stands for, a user-defined conversion function is invoked. For instance, a CharProxy can be converted into the char it stands for by calling operator char. As Item 5 explains, compilers may use only one user-defined conversion function when converting a parameter at a call site into the type needed by the corresponding function parameter. As a result, it is possible for function calls that succeed when passed real objects to fail when passed proxies. For example, suppose we have a TVStation class and a function, watchTV: class TVStation { public: TVStation(int channel); ... }; void watchTV(const TVStation& station, float hoursToWatch); Thanks to implicit type conversion from int to TVStation (see Item 5), we could then do this: watchTV(10, 2.5); // watch channel 10 for // 2.5 hours Using the template for reference-counted arrays that use proxy classes to distinguish lvalue and rvalue invocations of operator[], however, we could not do this: Array intArray; intArray[4] = 10; watchTV(intArray[4], 2.5); // error! no conversion // from Proxy to // TVStation Given the problems that accompany implicit type conversions, it's hard to get too choked up about this. In fact, a better design for the TVStation class would declare its constructor explicit, in which case even the first call to watchTV would fail to compile. For all the details on implicit type conversions and how explicit affects them, see Item 5. Evaluation Proxy classes allow you to achieve some types of behavior that are otherwise difficult or impossible to implement. Multidimensional arrays are one example, lvalue/rvalue differentiation is a second, suppression of implicit conversions (see Item 5) is a third. At the same time, proxy classes have disadvantages. As function return values, proxy objects are temporaries (see Item 19), so they must be created and destroyed. That's not free, though the cost may be more than recouped through their ability to distinguish write operations from read operations. The very existence of proxy classes increases the complexity of software systems that employ them, because additional classes make things harder to design, implement, understand, and maintain, not easier. Finally, shifting from a class that works with real objects to a class that works with proxies often changes the semantics of the class, because proxy objects usually exhibit behavior that is subtly different from that of the real objects they represent. Sometimes this makes proxies a poor choice when designing a system, but in many cases there is little need for the operations that would make the presence of proxies apparent to clients. For instance, few clients will want to take the address of an Array1D object in the two-dimensional array example we saw at the beginning of this Item, and there isn't much chance that an ArrayIndex object (see Item 5) would be passed to a function expecting a different type. In many cases, proxies can stand in for real objects perfectly acceptably. When they can, it is often the case that nothing else will do. Back to Item 29: Reference counting Continue to Item 31: Making functions virtual with respect to more than one object