Effective C + + learning notes (Clause 21: when an object must be returned, don't try to return its reference)

Recently, I started to watch Effective C + +. In order to facilitate future review, I specially took notes. If I misunderstand the knowledge points in the book, please correct!!!

In the previous article, we learned the high efficiency of reference passing, but this does not mean that value passing is useless. If you only want to use reference passing, you may make a fatal error: start passing some references to non-existent objects.

Design a class representing rational numbers, including a function to calculate the product of two rational numbers.

class Rational{  //Class representing rational numbers
public:
    Rational(int numerator = 0, int denominator = 1); //Clause 24 explains why this constructor is not declared explicit
    ...
private:
    int n,d;  //Numerator n and denominator d
    //Clause 3 explains why the return type is
  	friend const Rational operator*(const Rational& lhs, const Rational& rhs);
};

If the operator * returns a constant by value, the local copy will be generated at the call. Can we avoid the high cost of copy by returning a reference? Like this:

friend const Rational& operator*(const Rational& lhs, const Rational& rhs);

It's a beautiful idea. But remember that a reference is just a name, representing an existing object. Whenever you see a reference declaration, you should immediately ask yourself, what is its other name? Because it must be another name for something. If opeator * needs to return a reference, it must create an object and then return a reference to this object. Function creates new objects in two ways, in stack space or in heap space.

  • Stack space creation

    const Rational& operator*(const Rational& lhs, const Rational& rhs){
      	Rational result(lhs.n*rhs.n, lhs.d*rhs.d);//Create local variable
      	return result;
    }
    

    You must reject this because the return reference must be constructed by the constructor in order to avoid calling the constructor. More seriously, this function returns a reference to a local variable. The life cycle of the local variable is within the function. Once you exit the function, the local variable will be destroyed. Therefore, the function actually returns a reference to a nonexistent object.

  • Heap space creation

    const Rational& operator*(const Rational& lhs, const Rational& rhs){
      	Rational* result = new Rational(lhs.n*rhs.n, lhs.d*rhs.d);
      	return *result;
    } 
    

    In this way, it is also inevitable to call the constructor, because the allocated memory will complete the initialization action with an appropriate constructor. But a new problem arises: who will delete the new object?

    Rational w,x,y,z;
    w = x * y * z;		//Equivalent to operator*(operator*(x,y),z)
    

    Here, the same statement calls operator * twice, so if you use new twice, you need to delete twice. But there is no reasonable way for operator * users to make those delete calls, because they cannot get the pointer hidden behind the reference returned by operator *. This definitely leads to memory leaks.

Whether the above code creates an object return reference in the stack or in the heap, it cannot avoid the call of the constructor. Our goal is to avoid any constructor calls, so can we use static variables to achieve our goal?

const Rational& operator(const Rational& lhs, const Rational& rhs){
    static Rational result;  	//Create a static object
    result=.....;				//Multiply lhs by rhs and store the result in result
    return result;
}

This approach will affect multithreading security, but it is only an obvious hidden danger. There are deeper hidden dangers. Consider the following seemingly reasonable Code:

bool operator==(const Rational& lhs, const Rational& rhs);	//Overloading Rational's equivalent operator

Rational a,b,c,d;
...
if((a * b) == (c * d)){
	//When the products are equal
}else{
    //When the products are unequal
}

The value of expression (a * b) == (c * d) is always true, no matter what the value of a,b,c,d is. This expression is equivalent to operator==(operator*(a,b),operator*(c,d)), from which it can be seen that before operator = = is called, two operators * have been called, and their returned references point to the same object - static variable result. Compare yourself with yourself, and of course return true.

The correct way to write a function that "must return a new object" is to let that function return a new object.

inline const Rational operator*(const Rational& lhs, const Rational& rhs){
	return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

In general, when you have to choose between "returning a reference and returning a new object", your job is to pick the right one. The resulting cost, let the compiler reduce the cost for you as much as possible, because the C + + compiler has its own optimization function, and the generated machine code will improve efficiency without affecting the results within the observable range.

Note:

  • Never return a pointer or reference to a local variable, or a return reference to a heap allocation object, or a return pointer or reference to a local static object, and multiple such objects may be required at the same time. (Article 4 has an example of "reasonably returning a reference to a local static object in a single threaded environment")

Clause 22: declare member variables as private

Keywords: C++

Added by lazytiger on Tue, 09 Nov 2021 20:31:07 +0200