Reading Notes Objective c ++ Item 21 when you must return an object, do not try to return a reference, inclutiveitem
1. Question: Can a reference be returned when a function is required to return an object?
Once the programmer understands that passing by value may have an efficiency issue (Item 20), many of them have become Uploader and are determined to clear all the overhead caused by hidden passing by value. There is no slack in the pursuit of pure reference transfer (no additional construction or analysis), but they will always produce fatal errors:They start to pass references to non-existing objects.. This is not a good thing.
Consider a class that represents a rational number. It contains a function that multiply two rational numbers (Item 3 ):
1 class Rational { 2 3 public: 4 5 Rational(int numerator = 0, // see Item 24 for why this 6 7 int denominator = 1); // ctor isn’t declared explicit 8 9 ...10 11 private:12 13 int n, d; // numerator and denominator14 15 friend16 17 const Rational // see Item 3 for why the18 19 operator*(const Rational& lhs, // return type is const20 21 const Rational& rhs);22 23 };
Operator * returns results by value. If you are not worried about the overhead caused by calling the constructor and destructor of this object, you are evading your professional responsibilities. If this object is not necessary, you do not want to pay for the overhead of such an object. So the question is: is it necessary to generate this object?
2. Problem Analysis (1): If a reference is returned, a new object must be created for the returned reference.
If you can return a reference, you do not have to pay for it. But remember that reference is just an alias, an alias of an existing object. Whenever you declare a reference, you should immediately ask yourself who the alias is for, because it must be an alias for something. For operator *, if this function returns a reference, it must return a reference pointing to an existing Rational object, which contains the product of two objects.
There is no reason to assume that such an object already exists before operator * is called. That is, if you perform the following operations:
1 Rational a(1, 2); // a = 1/22 3 Rational b(3, 5); // b = 3/54 5 Rational c = a * b; // c should be 3/10
It is expected that a rational number with a value of 3/10 already exists. If operator * is about to return a reference to a rational number whose value is 3/10, it must be created by itself.
3. Problem Analysis (2): Three error methods for creating a new object 3.1 create an object pointed to by reference on the stack
A function can only create a new object in two ways:On Stack or on Stack. Create an object on the stack by defining a local variable. With this policy, you can try to implement it using the following method: operator *:
1 const Rational& operator*(const Rational& lhs, // warning! bad code! 2 3 const Rational& rhs) 4 5 { 6 7 Rational result(lhs.n * rhs.n, lhs.d * rhs.d); 8 9 return result;10 11 }
You will immediately reject this practice because your goal is to avoid calling constructor, but the result here must be constructed. A more serious problem is that the function returns a reference pointing to the result, but the result is a local object. When the function exits, the object will be destroyed. Therefore, operator * of this version does not return a reference pointing to Rational. It returns a reference pointing to the previous Rational object, which is now blank and annoying, the corpse of the rotten Rational object has been destroyed. Any caller using the return value of this function will immediately enter the scope of undefined behavior. The fact is,Any function that returns a reference to a local object is destroyed.. (The same is true for a function that returns a pointer to a local object ).
3.2 Create a reference object on the stack
Let's further consider the following usage possibilities:Create an object on the stack and return a reference pointing to it.. The object on the stack is created by using new, so you can implement a heap-based operator * as follows *:
1 const Rational& operator*(const Rational& lhs, // warning! more bad 2 3 const Rational& rhs) // code! 4 5 { 6 7 Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); 8 9 return *result;10 11 }
Here you still need to pay for the calling of the constructor. the initialization of the memory allocated for new is implemented by calling a suitable constructor, but there is another problem: who applies the new called delete on this object?
Even a conscientious and responsible caller can avoid Memory leakage in the following scenarios:
1 Rational w, x, y, z;2 3 w = x * y * z; // same as operator*(operator*(x, y), z)
Here, operator * is called twice in the same statement, so two new operations are used, and two delete operations are required to destroy the new objects. There is no reasonable way for operator * customers to make these calls, because for them there is no reasonable way to get the pointer hidden behind the reference returned from operator. This ensures resource leakage.
3.3 create a static object for reference 3.3.1 single static object
You may have noticed that,Whether on the stack or stackOperator *All returned results must call a constructor.. Maybe you can recall that our original intention is to avoid such a constructor call. You may think that you know a method that only needs to call the constructor once, and the rest of the constructor is avoided. The following implementation suddenly occurs. This method is based on the implementation of another operator *: it returns a reference pointing to a static Rational object. The function implementation is as follows:
1 const Rational& operator*(const Rational& lhs, // warning! yet more 2 3 const Rational& rhs) // bad code! 4 5 { 6 7 static Rational result; // static object to which a 8 9 // reference will be returned10 11 result = ... ; // multiply lhs by rhs and put the12 13 // product inside result14 15 return result;16 17 }
Like all designs that use static objects, this method adds thread-safe sorting, but this disadvantage is obvious. To look at the deeper defects, consider a completely reasonable customer code:
1 bool operator==(const Rational& lhs, // an operator== 2 3 const Rational& rhs); // for Rationals 4 5 Rational a, b, c, d; 6 7 ... 8 9 if ((a * b) == (c * d)) {10 11 do whatever’s appropriate when the products are equal;12 13 } else {14 15 do whatever’s appropriate when they’re not;16 17 }
Guess what? The result of the expression (a * B) = (c * d) is always true, regardless of the value of a, B, c, and d!
Rewrite the expression in the form of an equivalent function. The above incredible things can be easily understood:
1 if (operator==(operator*(a, b), operator*(c, d)))
Note that when operator = is called, operato * has been called twice. Each call will return a reference to the static Raitional object in operator. Therefore, operator = compares the static Rational object in operator * with the static Rational object in operator. It would be strange if they are not equal.
3.3.2 Static Array
This should be enough to convince you that it is a waste of time to return a reference from a function like operator *, but some people now think: Well, if a static is not enough, it is possible that a static array can achieve the goal...
I cannot provide sample code to make this design look so elegant, but I can describe why this idea makes you feel ashamed and blushing.FirstYou must select an appropriate n, that is, the size of the array. If n is too small, you may exhaust the space for storing the function return value, so that we do not get any benefit for the previous single static object design. If n is too large, the performance of your program will decrease, because even if this function is used only once, every object in the array will be constructed before the first call. This allows you to pay for calling n constructors and n destructor. If optimization is a process for improving software performance, it should be called pessimization ).LastImagine how you can put the values you need into an array object, and what the cost will be. The most direct method is to move values between objects by assigning values, but what is the cost of assigning values? For many types, the value assignment is equivalent to calling an destructor (releasing old values) and a constructor (Copying new values ). However, your goal is to avoid the overhead of structure analysis! This method does not work. (Replacing arrays with vectors does not improve the problem .)
4. Conclusion: The correct way to return a new object from a function is to return an object.
The correct way to implement a function that must return a new object is to let the function return a new object (value is not a reference ). For the Rational opertaor * function, it implements the following code (or equivalent code ):
1 inline const Rational operator*(const Rational& lhs, const Rational& rhs)2 3 {4 5 return Rational(lhs.n * rhs.n, lhs.d * rhs.d);6 7 }
Of course, you will introduce the constructor and destructor overhead from the operator * returned values, but in the long run, this is a small price for correct behavior. In addition, your bill will never come again. Like many programming languages, C ++ allows the compiler implementers to optimize the code without changing the visualized code behavior to improve the Code Generation performance. In some cases, we find that the Construction and Analysis of operator * return values can be safely eliminated. When the compiler uses this fact (the compiler often does this), your program will proceed in the way you expect, but faster than you want.
This clause comes down to the following: when a decision is made between returning a reference or returning an object,Your job is to choose the one that can provide the correct behavior. To solve the problem of "how to make this choice have as little overhead as possible", let the compiler supplier fight.
5. Summary
Never return a pointer or reference pointing to a local stack object, pointing to a reference to a heap object, or returning a pointer or reference pointing to a local static object when multiple objects are needed. (Item 4) a design example is provided, indicating that it is reasonable to return a reference pointing to a local static object, at least in a single-threaded environment .)