Once programmers turn their attention to the implicit efficiency problem of object value transfer (see article 20th), many people become extreme "Reform runners ", they put aside the root of the pass-through method. While they are indomitable in pursuit of the purity of the transfer-through method, they also made a fatal mistake: sometimes the referenced objects do not exist. This is by no means a good thing.
See the following example. The rational class is used to represent rational numbers. It also includes a function to calculate the product of two rational numbers:
Class rational {
Public:
Rational (INT Numerator = 0, int Denominator = 1 );
// The 24th clause explains why the constructor has no explicit declaration.
...
PRIVATE:
Int N, D; // molecule (N) and denominator (d)
Friend const rational
Operator * (const rational & LHS, const rational & RHs );
// The 3rd clause explains why the returned value is const.
};
Operator * of this version returns an object by passing values. If you do not consider the overhead of this object in the construction and analysis processes, you are evading your professional responsibilities. If you don't have to pay for such an object, you don't have. Now the question is: do you have to pay for this generation?
Okay. If you can return a reference as a substitute, you don't need it. However, remember that a reference is just a name, and it is an alias for an existing object. When you see a reference declaration, you should immediately ask yourself: what is its other name, because a reference must have its own name. Therefore, if operator * returns a reference, it must reference an existing rational object, this object contains the product of two objects that require multiplication.
If you want this object to exist before calling operator *, you are too irrational. That is to say, if you do this:
Rational A (1, 2); // A = 1/2
Rational B (3, 5); // B = 3/5
Rational c = a * B; // The value of C should be 3/10
It seems irrational to expect a rational number with a value of 3/10. Actually, this is not the case. If operator * returns a reference pointing to this type of value, it must create this number by itself.
A function can only create new objects in two ways: on the stack or on the stack. Defining a local variable is to create a new object on the stack. When this policy is applied, you may write operator * in this way *:
Const rational & operator * (const rational & LHS, const rational & RHs)
// Warning! Error Code
{
Rational result (LHS. N * RHS. N, LHS. D * RHS. D );
Return result;
}
You can deny this implementation because your goal is to prevent calls to the constructor, but the result will be initialized like other objects. A more serious problem is that this function will return a reference pointing to the result, but the result is a local object, and the local object will be destroyed when the function exits. In this version, operator * does not return a reference pointing to rational. It returns a reference pointing to a "Preliminary rational", which was a rational object, A dead, bad, rotten, once a rational corpse, but now it has nothing to do with rational because it has been destroyed. For all callers, as long as they touch the return value of this function a little, they will encounter endless unpredictable behavior. In fact, any function that returns a local object reference is disastrous. (This is also true for any function that returns a pointer to a local object .)
Now, let's consider the feasibility of the following practice: Create an object on the stack and return a reference pointing to it. Since the objects stored on the heap are created by new, you may write a heap-based operator * as follows *:
Const rational & operator * (const rational & LHS, const rational & RHs)
// Warning! There are more errors!
{
Rational * result = New Rational (LHS. N * RHS. N, LHS. D * RHS. D );
Return * result;
}
Okay, now you still need to pay the price for calling the constructor. This is because the memory allocated by new needs to be initialized by calling a suitable constructor, but now you are facing this another problem: who will ensure the execution of the delete statement corresponding to the new statement?
Even if the caller is very conscientious and responsible and has a good intention, they cannot guarantee that there will be no memory leakage in the following scenarios:
Rational w, x, y, z;
W = x * y * z; // equivalent to operator * (x, y), Z)
Here, there are two calls to operator * in a statement, so there are two new operations to be cleared using Delete. However, there is no reason to ask the client programmer of operator * to perform this operation, because a reference is returned for the call to operator, there is no reason to ask the client programmer to obtain the pointer hidden behind this reference. This will inevitably cause resource leakage.
However, you may have noticed that both the stack solution and the heap solution face the same problem: they all need to call the constructor once for every return value of operator. Maybe you can recall that our initial goal is to avoid calling constructor like this. Maybe you think you know a method to reduce the number of calls to this type of constructor to only one time. You may have thought of the following implementation method: Let operator * return a reference pointing to a static rational object, which is inside the function:
Const rational & operator * (const rational & LHS, const rational & RHs)
// Warning! There will be more and more errors!
{
Static rational result; // The static object used as the returned value.
Result =...; // multiply LHS and RHS and store the product to result
Return result;
}
Like other design methods that introduce static objects, this method significantly improves thread security, but it brings more obvious defects. The following client code is impeccable, but the above design exposes problems:
Bool operator = (const rational & LHS, const rational & RHs );
// Operator =
Rational a, B, c, d;
...
If (A * B) = (C * D )){
Perform appropriate operations when the product is equal;
} Else {
When the product is not equal, perform the appropriate operation;
}
Guess what will happen? No matter what value A, B, C, or D takes, the value of the expression (A * B) = (C * D) is always true.
We changed the form of the judgment statement in the above function, which is more simple:
If (operator = (operator * (a, B), operator * (c, d )))
Note that when operator = is called, there are two active calls to operator *. Each call returns a reference to a static rational object inside operator. Therefore, the compiler will require operator = to compare the static rational objects inside operator * with itself. If the results are not always equal, this is a strange thing.
The above content seems enough to convince you that returning a reference for a function like operator * is indeed a waste of time, but sometimes you will think: "Okay, if a static value is not enough, you can use a static array... "
I can't use examples to defend my point of view, but I can prove with very concise reasoning that this will make you more ashamed: First, you must determine an N value, that is, the size of the array. If n is too small, the storage space of the function return value may be used up, which is as bad as the solution for a single static object that has just been negated. However, if the value of N is too large, your program will face performance problems because every object in the array should be constructed when the function is called for the first time. This will make you pay N constructor calls and N constructor calls, even if the function we are discussing is called only once. If "optimization" is called a step to improve software performance, we can call it "degradation ". Finally, consider: how do you put the required values into the objects in the array? How much did you pay during the placement process? The most direct way to pass values between two objects is to assign values, but what overhead does the assignment operation incur? For many types, the overhead of value assignment is similar to calling an destructor (to destroy the old value) and adding a constructor (to copy the new value ). But you must know that your original goal is to avoid the overhead caused by the construction and analysis processes! Face it: this will not produce good results. (Don't think about it. Replacing arrays with vectors won't improve much .)
Compile a function that must return a new object. The correct method is to let this function return a new object. For the rational operator *, this means that the following code basically meets the requirements:
Inline const rational operator * (const rational & LHS, const rational & RHs)
{
Return rational (LHS. N * RHS. N, LHS. D * RHS. D );
}
Obviously, this may lead to the overhead of the construction and analysis process of operator *'s return value. However, in the long run, you can get more benefits by paying this small price. In addition, you may never have to pay for this horrible list. Just like other programming languages, C ++ allows the compiler's specific implementation version to improve performance by optimizing the code without changing its inherent behavior. In some cases, the construction and destructor of operator * return values can be safely excluded. When the compiler takes advantage of this in real time (the compiler usually does this), your program can continue to execute as expected, just faster.
In the final analysis, when you choose whether to use a reference to return or directly return an object, your job is to make the right decision so that the program has the right action. Then, leave the optimization work to the compiler manufacturers. They will make your choice as economical and practical as possible.
Keep in mind
- For partial/stack objects, if you need to use any of them as the return values of functions