C++ Tutorial
- Object Returning - 2020
When a function, either a member function or a standalone function, returns an object, we have choices.
The function could return
- A reference to an object
- A constant reference to an object
- An object
- A constant object
Though the main reason for using const reference is efficiency, there are restrictions on when this choice could be used. If a function returns an object that is passed to it, either by object invocation or as a method argument, we can increase the efficiency of the method by having it pass a reference.
For example, suppose we want to write a function Maximum() that returns the larger of two Complx objects, where the Complx is defined as below.
#include <iostream> #include <cmath> using namespace std; class Complx { private: double real; double imag; public: Complx() {} Complx(double r, double i): real(r), imag(i) {} Complx operator+(const Complx & c) const { return Complx(real + c.real, imag + c.imag); } Complx & operator=(const Complx &c;) { real = c.real; imag = c.imag; return *this; } friend ostream& operator<<(ostream &os;, const Complx &c;); double size() const { return sqrt(real*real + imag*imag); } }; ostream & operator<<(ostream &os;, const Complx & c) { os << "(" << c.real << "," << c.imag << ")"; return os; } /* Complx Maximum(const Complx &c1;, const Complx &c2;) { if (c1.size() > c2.size()) return c1; else return c2; } */ const Complx & Maximum(const Complx &c1;, const Complx &c2;) { if (c1.size() > c2.size()) return c1; else return c2; } int main( ) { Complx c1(10,30); Complx c2(13,25); Complx mx = Maximum (c1,c2); cout << " c1 = " << c1 << endl; cout << " c2 = " << c2 << endl; cout << "Maximum(c1,c2) = " << mx << endl; Complx c3 (20,40); Complx c4,c5; c5 = c4 = c3; cout << c4 << " got its value from c3" << endl; Complx c6 = c1 + c2; cout << " c6 (c1+c2) = " << c6 << endl; Complx c7; c1 + c2 = c7; return 0; }
Output is:
c1 = (10,30) c2 = (13,25) Maximum(c1,c2) = (10,30) (20,40) got its value from c3 c6 (c1+c2) = (23,55)
Either of the following two implementations for Maximum() would work:
// version #1 Complx Maximum(const Complx &c1;, const Complx &c2;) { if (c1.size() > c2.size()) return c1; else return c2; } // version #2 : This is our choice const Complx & Maximum(const Complx &c1;, const Complx &c2;) { if (c1.size() > c2.size()) return c1; else return c2; }
There are three points that can be emphasized:
- Returning an object invokes the copy constructor while returning a reference doesn't. So, the version #2 does less work and is more efficient.
- The reference should be to an object that exists when the calling function is execution. In this example, the reference is to either c1 or c2, and both are objects defined in the calling function, so the requirement is met.
- Both c1 or c2 are declared as being const references, so the return type has to be const to match.
There are two common examples of returning a non-const object
- Overloading the assignment operator
- Overloading the << operator for use with cout
The return value of operator==() is used for chained assignment:
Complx c3 (20,40); Complx c4,c5; c5 = c4 = c3;In the code, the return value of c4.operator=(c3) is assigned to c5. Returning either a Complx object or a reference to a Complx object would work. But using a reference allows the function to avoid calling the Complx copy constructor to create a new Complx object. In this case, the return type is not const because operator=() method returns a reference to c4, which is modified.
The return value of operator<<() is used for chained output:
cout << c4 << " got its value from c3" << endl;
In the code, the return value of operator<<(cout,c4) becomes the object used to display the string " got its value from c3". Here, the return type must be ostream & and not just ostream. Using an ostream return type would require calling the ostream copy constructor, and, as it turns out the ostream class does not have a public copy constructor. Fortunately, returning a reference to cout poses no problems because cout is already in scope of the calling function.
Also, look at another case when we need to return a reference to an object related to the memory allocation in the constructor and copy constructor.
If the object being returned is local to the called function, then it should not be returned by reference because the local object has its destructor called when the function terminates. This kind of memory management is called automatic memory management, and it is associated with local variables/objects. A local object occupies memory that the system allocates. The system automatically deallocates that memory at the end of the block that contains the definition. So, when control returns to the calling function, there is no object left to which the reference can refer or the pointer can point to. When the function returns, it's the end of the execution block that contains the definition of the reference or pointer. Because of the deallocation, the reference/pointer is now invalid, but the function tries to return it any way. What would happen? It's anybody's guess.
In these circumstances, we should return an object, not a reference. Never Retrun a reference to a local object.
For example, assume we have a function returning a local object like this:
const Complx &complxReturn;(const Complx & c) { Complx complxObj = c; return complxObj; }
When the function completes, the storage in which the local objects were allocated is freed. A reference to a local object refers to undefined memory after the function terminates. So, the function may fail at run time because it returns a reference to a local object. The functions ends, the storage in which complxObj resides is freed. The return value refers to memory that is no longer available to the program.
Usually, overloaded arithmetic operators fall into this category. In our example, we have overloaded operator+:
Complx operator+(const Complx & c) const { return Complx(real + c.real, imag + c.imag); }
When we do
Complx c6 = c1 + c2;
The value being returned is not c1, which should be left unchanged by the process, nor c2, which should also be unaltered. Thus the return value can't be a reference to an object that is already present in the calling function. Instead, the sum is a new, temporary object computed from Complx operator+(), and the function shouldn't return a reference to a temporary object, either. Instead, it should return an actual object, not a reference:
Complx operator+(const Complx & c) const { return Complx(real + c.real, imag + c.imag); }
There is the added expense of calling the copy constructor to create the returned object so than the calling function can use this copy of the object, but the extra expense is unavoidable. Using an object return type, however, means the program constructs a copy of Complx before destroying it, and the calling function gets the copy. (See when we call copy constructor: copy constructor)
The Complx::operator+() in the example has a strange property. The intended use is this:
Complx c6 = c1 + c2; // #1
But the definition also allows us to use the following:
Complx c7; c1 + c2 = c7; // #2
This code is possible because the copy constructor constructs a temporary object to represent the return value. So, in the code, the expression c1 + c2 stands for that temporary object. In statement #1, the temporary object is assigned to c6. In statement #2, c7 is assigned to the temporary object.
The temporary object is used and then discarded. For instance, in statement #2, the program computes the sum of c1 and c2, copies the answer into the temporary return object, overwrites the contents with the contents of c7, and then discards the temporary object. The original complex numbers are all left unchanged.
If we declare the return type as a const object, we can avoid the problem.
const Complx operator+(const Complx & c) const { return Complx(real + c.real, imag + c.imag); }
Why do we use const for a reference return?
Let's look at the following example:
#include <iostream> struct node { int data; }; const node & makeNode(node &cref;); int main() { node nodeA = {0}; std::cout << "1: nodeA.data = " << nodeA.data << std::endl; makeNode(nodeA); std::cout << "2: nodeA.data = " << nodeA.data << std::endl; node nodeB; nodeB = makeNode(nodeA); std::cout << "3: nodeA.data = " << nodeA.data << std::endl; std::cout << "1: nodeB.data = " << nodeB.data << std::endl; node nodeC; // makeNode(nodeB).data = 99; return 0; } const node & makeNode(const node &ref;) { std::cout << "call makeNode()\n"; ref.data++; const return ref; }
The makeNode() function return type is const node &. What's the purpose of const? It does not mean that the node structure itself is const; it just means that we can't use the return value directly to modify the structure.
If we omitted const, we could use the code below:
makeNode(nodeB).data = 99;Because makeNode returns a reference to nodeB, this code is the same as this:
makeNode(NodeB); nodeB.data = 99;But because of the const, we can't use the return value directly to modify the structure, and with the const, the code:
makeNode(nodeB).data = 99;won't compile.
- If a method or function returns a local object, it should return an object, not a reference.
- If a method or function returns an object of a class for which there is no public copy constructor, such as ostream class, it must return a reference to an object.
- Some methods and functions, such as the overloaded assignment operator, can return either an object or a reference to an object. The reference is preferred for reasons of efficiency.
- It is an error to return a pointer to a local object. Once the function completes, the local object are freed. The pointer would be a dangling pointer that refers to a nonexistent object.
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization