⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 mi30.htm

📁 一个非常适合初学者入门的有关c++的文档
💻 HTM
📖 第 1 页 / 共 2 页
字号:
 More Effective C++ | Item 30: Proxy classes Back to Item 29: Reference countingContinue to Item 31: Making functions virtual with respect to more than one objectItem 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 20The 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}                                           // compilationIt's not even legal for a heap-based allocation: int *data =  new int[dim1][dim2];                      // error!Implementing Two-Dimensional ArraysMultidimensional 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 T>class Array2D {public:  Array2D(int dim1, int dim2);  ...};Now we can define the arrays we want: Array2D<int> data(10, 20);             // fineArray2D<float> *data =  new Array2D<float>(10, 20);          // finevoid processInput(int dim1, int dim2){  Array2D<int> 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 T>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 T>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];          // fineWhat 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 T>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<float> data(10, 20);...cout << data[3][6];          // fineHere, 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...                      // interfacecout << s1[5];           // read s1s2[5] = 'x';             // write s2s1[3] = s2[8];           // write s1, read s2Note 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 consts2[5] = 'x';                          // also calls non-const                                      // operator[]: s2 isn't consts1[3] = s2[8];                        // both calls are to non-const                                      // operator[], because both s1                                      // and s2 are non-const objectsOverloading 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<StringValue> 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 workss2[5] = 'x';             // also legal, also workss1[3] = s2[8];           // of course it's legal,                         // of course it works

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -