More Effective C ++ Item M30: Agent

zhaozj2021-02-16  45

1.2 Item M30: Agent

Although you can live in the same location with your family, it is usually not the case. Unfortunately, C has not realized this fact. At least, some signs can be seen from its support for arrays. In Fortran, Basic or even COBOL, you can create two-dimensional, three-dimensional or even N-dimensional arrays (OK, Fortran can only create up to 7 dimensional arrays, but don't be too hot). But in C ? Just sometimes, and just some degree.

This is legal:

INT DATA [10] [20]; // 2D Array: 10 by 20

And the same structure is not possible if the size of the variable is used, it is not possible:

Void ProcessInput (Int Dim1, Int Dim2)

{

INT DATA [DIM1] [DIM2]; // Error! Array Dimensions

... // Must Be Known During

}

Even, it is illegal:

INT * DATA =

New Int [DIM1] [DIM2]; // Error!

* Implement two-dimensional array

The multi-dimensional array is the same with the use of C , so it is important to find a similar support method. The common method is the standard method in C : use a class to achieve what we need and the C language does not provide. Therefore, we can define a class template to implement two-dimensional arrays:

Template

Class array2d {

PUBLIC:

Array2D (int Dim1, Int Dim2);

...

}

Now we can define the arrays we need:

Array2D Data (10, 20); // Fine

Array2D * data =

New array2d (10, 20); // Fine

Void ProcessInput (Int Dim1, Int Dim2)

{

Array2D Data (DIM1, DIM2); // Fine

...

}

However, using these Array objects is not directly. According to the syntax habits in C and C , we should be able to use [] to the index group:

COUT << Data [3] [6];

But how should we declare the subscription operation in the Array2D class so that we can do this?

Our initial impulse may be a statement of an Operator [] [] function:

Template

Class array2d {

PUBLIC:

// Declarations That Won't compile

T & Operator [] [] (int index1, int index2);

Const T & Operator [] [] (int index1, int index2) const;

...

}

However, we will soon stop this impulse because there is no Operator [] [] this kind of thing, don't expect your compiler to let go. (All operators that can be overloaded are Item M7.) We have to stove.

If you can tolerate the strange syntax, you may learn other languages ​​() to the quotation group. Do this, you only need to overload Operator (): Template

Class array2d {

PUBLIC:

// Declarations That Will Compile

T & Operator () (int index1, int index2);

Const T & Operator () (int index1, int index2) const;

...

}

Users so use arrays:

COUT << Data (3, 6);

This is easy to implement and it is easy to promote to any multi-dimensional array. Disadvantages is that your Array2D object looks unlikely a point in the embedded array. In fact, the operation of the above access element (3, 6) looks like a function call.

If you refuse to let the access array behavior look like a fortran, you will once again use []. Although there is no Operator [] [], but the following code is legal:

INT DATA [10] [20];

...

COUT << Data [3] [6]; // Fine

What did you explain?

Note, variable Data is not a real 2D array, it is a 10-element one-dimensional array. Each element is a 20-element array, so the expression DATA [3] [6] is actually (Data [3]) [6], that is, the fourth element of the fourth element of this array. element. Briefly, the first [] returned is an array, the second [] takes one element from this returned array.

We can play the same trick through the Operator [] of the Array2D class. Array2D Operator [] Returns a new class Array1D object. Rebursing Array1D's Operator [] to return the elements in the desired 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 [] () const;

...

}

Now it is legal:

Array2D DATA (10, 20);

...

COUT << Data [3] [6]; // Fine

Here, DATA [3] returns an Array1D object that returns the Operator [] operation on this object returns the floating point number in the 2D array (3, 6).

Users from Array2D do not need to know the existence of Array1D classes. This "one-dimensional array" object behind this is not only the user of the Array2D class. It is like the user's programming as they are using a true two-dimensional array. It is meaningless to do so for the Array2D class: In order to meet the repetition of C , these objects must be compatible with the elements of the syntax in another one-dimensional array of arrays.

Each Array1D object plays a one-dimensional array, and this one-dimensional array does not appear in a program that uses Array2D. Objects playing Other objects are often referred to as an agent class. In this example, Array1D is an agent class. Its instance plays a one-dimensional array of concepts that do not exist. (Proxy Object) and proxy classs are not very common; such objects are sometimes called Surrogate.) * Distinguish whether read operations or write operations by Operator []

Using a proxy to implement a multi-dimensional array is a very common method, but the purpose of the agent class is far more than this. For example, Item M5 demonstrates how the agent class can be used to block the constructor of a single parameter is misused as the type conversion function. In each of the proxy class, the most amazing is that the help distinguished by Operator [] or write operation.

Consider a string type with the reference count and support Operator []. The details of such classes are seen in Item M29. If you still don't understand the concept behind the reference, you will be a good idea to be familiar with the contents of Item M29.

Support Operator [] String type, allowing users to be such code:

String S1, S2; // a string-like class; the

// use of proxies keeps this

// Class from conforming to

// the Standard String

... // Interface

COUT << S1 [5]; // r r

S2 [5] = 'x'; // Write S2

S1 [3] = S2 [8]; // Write S1, READ S2

Note that Operator [] can call in two different situations: read a character or write a character. Read is a right value; write is a left value. (This noun comes from the compiler, the left value appears on the left side of the assignment operation, the right value appears on the right side of the assignment operation.) Typically, use an object to do the left value means it may be modified, do the right value means it Can't be modified.

We want to distinguish Operator [] as the left value or right, because for the data structure with reference count, the cost of the read operation can be much smaller than the cost of the write operation. As explained in Item M29, the write operation of the reference count object will result in a copy of the entire data structure, and read unnecessary, as long as a value is simply returned. Unfortunately, in Operator [] inside, there is no way to determine how it is called, it is impossible to distinguish it is to do the left value or right value.

"But,",, "You call," We don't need it. We can reload Operator [] based on const attributes, so you can distinguish it or write. "In other words, you suggest us to solve the problem:

Class string {

PUBLIC:

Const Char & Operator [] (int index) const; // for reads

Char & operator [] (int index); // for WRITES

...

}

Oh, this can't work. The compiler selects the const and non-Const versions of this member function based on the const attribute of the object to call the member function, regardless of the environment in which the call is called. therefore:

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 TO NON-CONST

// Operator [], BECAUSE BOTH S1

// and s2 area non-const objects

So, overloading Operator [] Failure to distinguish it or write.

In Item M29, we succumbed to this unsatisfactory state, and conservative assume that all Operator [] calls are written. This time, we will not give up so easy. Maybe it is impossible to distinguish the left value in Operator [] or the right value, but we still want to distinguish them. So we will look for a way. If you let you be restricted by it, what happily is there?

Our method is based on this fact: maybe it is impossible to distinguish the left value in Operator [], but we can still distinguish between read operations and write operations, if we will judge whether reading or writing behavior is postponed to us know Operator [ The result of how to use it. What we need is that there is a method to postpone the read or write judgment after Operator [] returns. (This is an example of the Lazy principle (see Item M17).)

The Proxy class allows us to get what we need, because we can modify how Operator [] let it return a (proxy) Proxy object rather than the character itself. We can wait to see how this proxy is used. If you are reading it, we can conclude that the call to Operator [] is read. If it is written, we must write the call processing of Operator [].

We will come to see the code, but we must first understand the Proxy class we use. I can only do three things on the Proxy class:

* Create it, that is, specify which character is played.

* Target it as an assignment operation, in which case the assignment can really act on the characters it played. When this is used, the Proxy class plays a left value.

* Use it in other ways. At this time, the proxy class played the right value.

Here is a String class with reference to a reference to use as a proxy class to distinguish Operator [] is an example of using the left value or the right value:

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 & these Proxy Pertains Toint Charindex; // Char With 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;

}

In addition to the increased CharProxy class (we will explain the following), this String class is compared to the final version in Item M29, all Operator [] functions now return to the CharProxy object. However, users of the String class can ignore this, and as the Operator [] returned is still the usual form (or its reference, see Item m1) to program:

String S1, S2; // Reference-Counted Strings

// use 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

Interesting is not it works, but why can it work.

First see this statement:

COUT << S1 [5];

Expression S1 [5] returns a charProxy object. There is no output stream operation for such objects, so the compiler works hard to find an implicit type conversion so that operator << calls success (see Item M5). They found a: there is an implicit conversion to char to CAHRPROXY class. So this conversion operation is called automatically, the result is the character of the CharProxy class being printed. This CharProxy to Char conversion is a typical behavior that occurs when all proxy objects are used.

The process when the left value is different. Look at:

S2 [5] = 'x';

As in front, the expression S2 [5] returns a charProxy object, but this time it is the target of assignment. Since the target of assignment is a CharProxy class, the assignment operation in the CharProxy class is called. This is important because we know that the value of the CharProxy object is used by the value of the value of the value of the CHARPROY. Therefore, we know that the Characters in the Proxy class are used by the left value, and some necessary operations must be performed to implement the left value of characters.

Similarly, statement

S1 [3] = S2 [8];

Call the assignment operation between the two CharProxy objects, inside this, we know one of the left values ​​on the left, right.

"Yeah, yeah!" You call, "I will show me." OK, this is the code of string Opertator [] function:

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 creates and returns a proxy object instead of characters. There is no action on that characters at all: We postpone it until we know whether reading or writing.

Note that the Const version of Operator [] returns a const of the Proxy object. Because CharProxy :: Operator = is a non-Const member function, such a proxy object cannot be used for assignment. Therefore, whether it is a proxy object returned from the const versions of Operator [], or it does not use the characters it played. Very convenient, it is exactly the behavior of the const version we want.

Also pay attention to the const_cast (see Item M2) of * this when CONST Operator [] returns to create a CharProxy object. This makes it satisfying the need for the constructor of the CharProxy class, and its constructor only accepts a non-Const's String class. Type conversion is usually the uneasy, but here, the CharProxy object returned by Operator [] is constant, so you don't have to worry that the String internal characters may be modified through the Proxy class.

The proxy object returned via Operator [] records which string object and the subscript of the character played:

String :: Charproxy :: CharProxy (String & Str, Int Index)

: theString (STR), Charindex (INDEX) {}

It is easy to use the Proxy object to right. It is possible to use the characters it played with:

String :: CharProxy :: operator char () const

{

Return thestring.value-> data [charindex];

}

If you have forgotten the Value member of the String object, please review the Item M29 to enhance your memory. Because this function returns a value of a character, and because the C defines such a value returned by the value, it can only be used by the value, so this conversion function can only appear in the right position.

Looking back, watching the implementation of CahrProxy's assignment operation, this is where we must handle the target (ie, left value) used by the characters played by the Proxy object. We can implement the assignment of CharProxy 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 * thisthestring.value-> data [charIndex] =

RHS.THESTRING.VALUE-> Data [rhs.charindex];

Return * this;

}

If you compare the non-const :: Operator [] in Item M29, you will see them extremely similar. This is expected. In Item M29, we pessimously assume that all the calls of all non-const port is written, so it is achieved. Now, we will move the implementation of the write into the assignment operation of CharProxy, so that you can avoid the call of non-Const's Operator [] is just the price of the write operation that is made when the right value is made. Just mention, this function needs to access the String's private data member Value. This is the reason for the Friends of CharProxy stted as String.

The second charProxy assignment operation is similar:

String :: Charproxy & String :: CharProxy :: Operator = (char C)

{

IF (theString.Value-> isshared ()) {

THESTRING.VALUE = New StringValue (theString.Value-> Data);

}

THHESTRING.VALUE-> DATA [Charindex] = C;

Return * this;

}

As a senior software engineer, you must of course eliminate code repeat in these two assignments, you should put them in a private member function for both calls. Are you not such a person?

* Limitations

Using the Proxy class is a good way to distinguish Operator [] to the left value or the right value, but it does not have no shortcomings. We like the seamless replacement of the Proxy object to the object they play, but it is difficult to achieve. This is because the right value is not just the case of assignment, then the behavior of the Proxy object is inconsistent with the actual object.

Take a look at the code from Item M29 to prove why we add a shared flag for each StringValue object. If string :: operator [] returns a CharProxy instead of char &, the following code will not compile:

String S1 = "Hello";

Char * p = & s1 [1]; // error!

Expression S1 [1] returns a charProxy, so the right side of "=" is a charProxy *. There is no conversion function from CharProxy * to Char *, so the initialization process of P failed. Typically, the operation of the proxy object address is taken with the pointer to take the operation of the actual object address, which is different.

To eliminate this difference, you need to overload the CharProxy class to take the address operation:

Class string {

PUBLIC:

Class charproxy {

PUBLIC:

...

Char * Operator & ();

Const char * Operator & () const;

...

}

...

}

These functions are easy to implement. The Const version returns the Const type pointer of the character placed:

Const Char * String :: CharProxy :: Operator & () Const

{

Return & (TheString.Value-> Data [Charindex]);

}

Non-Const versions have more operations because the characters of the pointer instructions it returns can be modified. It is similar to the non-constical string :: operator [] behavior in Item M29, and it is also very close to: 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]);

}

Its code and other member functions of CharProxy have a lot, so I know that you will cover them into a private member function.

CHAR and the second different differences of CharProxy in the agent appear in an array template with a reference count If we want to use the Proxy class to distinguish its Operator [] When the left value is still right:

Template // Reference-Counted Array

Class Array {// use 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);

...

}

Look at this array may be used:

Array Intarray;

...

INTARRAY [5] = 22; // Fine

INTARRAY [5] = 5; // Error!

INTARRAY [5]; // Error!

As we expected, it is successful when operator [] is the simplest assignment operation, but when it appears Operator = and Operator , it failed. Because Operator [] returns a proxy object, it does not have Operator = and Operator operations. The same situation exists in other operations that require the left value, including Operator * =, Operator << =, Operator -, etc. If you want these to do you in Operator [], you must define all of these functions for the Arrax-T> :: Proxy class. This is a very large number of work, you may not be willing to do. Unfortunately, you either do these jobs, or there is no such operation, can't be two. A similar problem must face: Call the actual object of the actual object through the Proxy object. Want to avoid it is impossible. For example, assuming that we have a reason for the array with reference to a reference count. We will define a Rational class and then use the Array Template you see in front:

Class russ {

PUBLIC:

Rational (int name = 0, int devenoman = 1);

INT numerator () const;

INT Denominator () Const;

...

}

Array Array;

This is what we expect, but we are very disappointed:

Cout << array [4] .Numerator (); // error!

INT DENOM = array [22] .denomanator (); // Error!

Now, the difference is very clear; Operator [] returns a proxy object instead of the actual Rational object. But the member function Numerator () and Denominator () are only on the Rational object instead of its proxy object. Therefore, your compiler has issued complaints. To make the behavior of the Proxy object, the object they play, you must override each function that can act on the actual object.

Another proxy object replaces the actual object failure is a reference to a non-Const:

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 stay fix the

// Problem, But it't

// compile

String :: Operator [] Returns a CharProxy object, but the swap () function requires that it is the char & type. A CharProxy object can be converted to a char in inverted, but is not converted to a conversion function of Char &. And it may be converted to the CHAR of SWAP, because this char is a temporary object (which is the return value of Operator char ()), according to the interpretation of Item M19, refuses to bind the temporary object to non-Const The referenced ginseng is reasonable.

The last proxy object cannot seamlessly replace the actual object is implicit type conversion. When the Proxy object is implicitly converted to the actual object it played, a user-defined conversion function is called. For example, a CharProxy object can be converted to it played with a CHAR that is called by calling the Operator char () function. As Item M5 is explained, the compiler is called when the function is called to this function, and only one user-defined conversion function is called. Thus, it is likely that the actual object is successful when the function is called, the proxy object is failed. For example, we have a TVStation class and a function watchtv (): class TVStation {

PUBLIC:

TvStation (int channel);

...

}

Void Watchtv (Const TVStation & Station, Float Hourstowatch);

We can do this with INT to TVStation (see Item M5).

Watchtv (10, 2.5); // Watch Channel 10 for

// 2.5 Hours

However, when using the array template with the proxy class with the proxy class, the array template of the reference count, we cannot do this:

Array Intarray;

INTARRAY [4] = 10;

Watchtv (Intarray [4], 2.5); // Error! No Conversion

// from proxy to

// tvStation

It is difficult to resolve due to the problem of implicit type conversion. In fact, a better design should be the application of its constructor to explicit, so that the first call for WatchTV () is compiled and failed. Details of implicit type conversion and EXPlicit have an effect of this Item M5.

* Review

The Proxy class can complete some other methods that are difficult to even implement. The multi-dimensional array is an example, the distinction between the left / right value is the second, restricting implicit type conversion (see Item M5) is the third.

At the same time, the Proxy class also has a disadvantage. As a function return value, the Proxy object is a temporary object (see Item 19), which must be constructed and destructure. This is not free, although this payment can get more compensation from the ability to distinguish the reading and writing. The existence of Proxy objects adds the complexity of the software because additional classes make things more difficult to design, implement, understand, and maintain.

Finally, the class from a class that handles the actual object to the class to process the proxy object, often change the semantics of the class, because the proxy object usually exhibits a subtle difference with the actual object. Sometimes, this makes it unable to choose to use the Proxy object when designing the system, but in many cases, there is a lot of operations to expose the Proxy object to the user. For example, there are very few users who take the address of the Array1D object in the above two-dimensional array example, nor how to pass the objects (see Item M5) of the subscript index (see Item M5) to a desired other type of function. In many cases, the Proxy object can perfectly replace the actual object. When they can work, there is usually the case where there is no other method.

转载请注明原文地址:https://www.9cbs.com/read-26735.html

New Post(0)