C++

Passing By Reference For Efficiency

Each time you pass an object into a function by value, a copy of the object is made. Each time you return an object from a function by value, another copy is made.

In the "Extra Credit" section at the end of Unit 5, you learned that these objects are copied onto the stack. Doing so takes time and memory. For small objects, such as the built-in integer values, this is a trivial cost.

However, with larger, user-created objects, the cost is greater. The size of a user-created object on the stack is the sum of each of its member variables. These, in turn, can each be user-created objects, and passing such a massive structure by copying it onto the stack can be very expensive in performance and memory consumption.

There is another cost as well. With the classes you create, each of these temporary copies is created when the compiler calls a special constructor: the copy constructor. Tomorrow you will learn how copy constructors work and how you can make your own, but for now it is enough to know that the copy constructor is called each time a temporary copy of the object is put on the stack.

When the temporary object is destroyed, which happens when the function returns, the object’s destructor is called. If an object is returned by the function by value, a copy of that object must be made and destroyed as well.

With large objects, these constructor and destructor calls can be expensive in speed and use of memory. To illustrate this idea, Listing 9.9 creates a stripped-down user-created object: SimpleCat. A real object would be larger and more expensive, but this is sufficient to show how often the copy constructor and destructor are called.

Listing 9.10 creates the SimpleCat object and then calls two functions. The first function receives the Cat by value and then returns it by value. The second one receives a pointer to the object, rather than the object itself, and returns a pointer to the object.

Listing 9.10. Passing objects by reference.

1: //Listing 9.10

2: // Passing pointers to objects

3:

4: #include <iostream.h>

5:

6: class SimpleCat

7: {

8: public:

9: SimpleCat (); // constructor

10: SimpleCat(SimpleCat&); // copy constructor

11: ~SimpleCat(); // destructor

12: };

13:

14: SimpleCat::SimpleCat()

15: {

16: cout << "Simple Cat Constructor...\n";

17: }

18:

19: SimpleCat::SimpleCat(SimpleCat&)

20: {

21: cout << "Simple Cat Copy Constructor...\n";

22: }

23:

24: SimpleCat::~SimpleCat()

25: {

26: cout << "Simple Cat Destructor...\n";

27: }

28:

29: SimpleCat FunctionOne (SimpleCat theCat);

30: SimpleCat* FunctionTwo (SimpleCat *theCat);

31:

32: int main()

33: {

34: cout << "Making a cat...\n";

35: SimpleCat Frisky;

36: cout << "Calling FunctionOne...\n";

37: FunctionOne(Frisky);

38: cout << "Calling FunctionTwo...\n";

39: FunctionTwo(&Frisky);

40: return 0;

41: }

42:

43: // FunctionOne, passes by value

44: SimpleCat FunctionOne(SimpleCat theCat)

45: {

46: cout << "Function One. Returning...\n";

47: return theCat;

48: }

49:

50: // functionTwo, passes by reference

51: SimpleCat* FunctionTwo (SimpleCat *theCat)

52: {

53: cout << "Function Two. Returning...\n";

54: return theCat;

55: }

OUTPUT: 

1: Making a cat...

2: Simple Cat Constructor...

3: Calling FunctionOne...

4: Simple Cat Copy Constructor...

5: Function One. Returning...

6: Simple Cat Copy Constructor...

7: Simple Cat Destructor...

8: Simple Cat Destructor...

9: Calling FunctionTwo...

10: Function Two. Returning...

11: Simple Cat Destructor...

NOTE: Line numbers will not print. They were added to aid in the analysis.

ANALYSIS: A very simplified SimpleCat class is declared on lines 6-12. The constructor, copy constructor, and destructor all print an informative message so that you can tell when they’ve been called. On line 34, main() prints out a message, and that is seen on output line 1. On line 35, a SimpleCat object is instantiated. This causes the constructor to be called, and the output from the constructor is seen on output line 2.

On line 36, main() reports that it is calling FunctionOne, which creates output line 3. Because FunctionOne() is called passing the SimpleCat object by value, a copy of the SimpleCat object is made on the stack as an object local to the called function. This causes the copy constructor to be called, which creates output line 4.

Program execution jumps to line 46 in the called function, which prints an informative message, output line 5. The function then returns, and returns the SimpleCat object by value. This creates yet another copy of the object, calling the copy constructor and producing line 6.

The return value from FunctionOne() is not assigned to any object, and so the temporary created for the return is thrown away, calling the destructor, which produces output line 7. Since FunctionOne() has ended, its local copy goes out of scope and is destroyed, calling the destructor and producing line 8.

Program execution returns to main(), and FunctionTwo() is called, but the parameter is passed by reference. No copy is produced, so there’s no output. FunctionTwo() prints the message that appears as output line 10 and then returns the SimpleCat object, again by reference, and so again produces no calls to the constructor or destructor.

Finally, the program ends and Frisky goes out of scope, causing one final call to the destructor and printing output line 11.

The net effect of this is that the call to FunctionOne(), because it passed the cat by value, produced two calls to the copy constructor and two to the destructor, while the call to FunctionTwo() produced none.

Passing a const Pointer

Although passing a pointer to FunctionTwo() is more efficient, it is dangerous. FunctionTwo() is not allowed to change the SimpleCat object it is passed, yet it is given the address of the SimpleCat. This seriously exposes the object to change and defeats the protection offered in passing by value.

Passing by value is like giving a museum a photograph of your masterpiece instead of the real thing. If vandals mark it up, there is no harm done to the original. Passing by reference is like sending your home address to the museum and inviting guests to come over and look at the real thing.

The solution is to pass a const pointer to SimpleCat. Doing so prevents calling any non-const method on SimpleCat, and thus protects the object from change. Listing 9.11 demonstrates this idea.

Listing 9.11. Passing const pointers.

1: //Listing 9.11

2: // Passing pointers to objects

3:

4: #include <iostream.h>

5:

6: class SimpleCat

7: {

8: public:

9: SimpleCat();

10: SimpleCat(SimpleCat&);

11: ~SimpleCat();

12:

13: int GetAge() const { return itsAge; }

14: void SetAge(int age) { itsAge = age; }

15:

16: private:

17: int itsAge;

18: };

19:

20: SimpleCat::SimpleCat()

21: {

22: cout << "Simple Cat Constructor...\n";

23: itsAge = 1;

24: }

25:

26: SimpleCat::SimpleCat(SimpleCat&)

27: {

28: cout << "Simple Cat Copy Constructor...\n";

29: }

30:

31: SimpleCat::~SimpleCat()

32: {

33: cout << "Simple Cat Destructor...\n";

34: }

35:

36:const SimpleCat * const FunctionTwo (const SimpleCat * const theCat);

37:

38: int main()

39: {

40: cout << "Making a cat...\n";

41: SimpleCat Frisky;

42: cout << "Frisky is " ;

43 cout << Frisky.GetAge();

44: cout << " years _old\n";

45: int age = 5;

46: Frisky.SetAge(age);

47: cout << "Frisky is " ;

48 cout << Frisky.GetAge();

49: cout << " years _old\n";

50: cout << "Calling FunctionTwo...\n";

51: FunctionTwo(&Frisky);

52: cout << "Frisky is " ;

53 cout << Frisky.GetAge();

54: cout << " years _old\n";

55: return 0;

56: }

57:

58: // functionTwo, passes a const pointer

59: const SimpleCat * const FunctionTwo (const SimpleCat * const theCat)

60: {

61: cout << "Function Two. Returning...\n";

62: cout << "Frisky is now " << theCat->GetAge();

63: cout << " years old \n";

64: // theCat->SetAge(8); const!

65: return theCat;

66: }

OUTPUT:

 Making a cat...

Simple Cat constructor...

Frisky is 1 years old

Frisky is 5 years old

Calling FunctionTwo...

FunctionTwo. Returning...

Frisky is now 5 years old

Frisky is 5 years old

Simple Cat Destructor...

ANALYSIS: SimpleCat has added two accessor functions, GetAge() on line 13, which is a const function, and SetAge() on line 14, which is not a const function. It has also added the member variable itsAge on line 17. The constructor, copy constructor, and destructor are still defined to print their messages. The copy constructor is never called, however, because the object is passed by reference and so no copies are made. On line 41, an object is created, and its default age is printed, starting on line 42.

On line 46, itsAge is set using the accessor SetAge, and the result is printed on line 47. FunctionOne is not used in this program, but FunctionTwo() is called. FunctionTwo() has changed slightly; the parameter and return value are now declared, on line 36, to take a constant pointer to a constant object and to return a constant pointer to a constant object.

Because the parameter and return value are still passed by reference, no copies are made and the copy constructor is not called. The pointer in FunctionTwo(), however, is now constant, and thus cannot call the non-const method, SetAge(). If the call to SetAge() on line 64 was not commented out, the program would not compile.

Note that the object created in main() is not constant, and Frisky can call SetAge(). The address of this non-constant object is passed to FunctionTwo(), but because FunctionTwo()’s declaration declares the pointer to be a constant pointer, the object is treated as if it were constant!

References as an Alternative

Listing 9.11 solves the problem of making extra copies, and thus saves the calls to the copy constructor and destructor. It uses constant pointers to constant objects, and thereby solves the problem of the function changing the object. It is still somewhat cumbersome, however, because the objects passed to the function are pointers.

Since you know the object will never be null, it would be easier to work with in the function if a reference were passed in, rather than a pointer. Listing 9.12 illustrates this.

Listing 9.12. Passing references to objects.

1: //Listing 9.12

2: // Passing references to objects

3:

4: #include <iostream.h>

5:

6: class SimpleCat

7: {

8: public:

9: SimpleCat();

10: SimpleCat(SimpleCat&);

11: ~SimpleCat();

12:

13: int GetAge() const { return itsAge; }

14: void SetAge(int age) { itsAge = age; }

15:

16: private:

17: int itsAge;

18: };

19:

20: SimpleCat::SimpleCat()

21: {

22: cout << "Simple Cat Constructor...\n";

23: itsAge = 1;

24: }

25:

26: SimpleCat::SimpleCat(SimpleCat&)

27: {

28: cout << "Simple Cat Copy Constructor...\n";

29: }

30:

31: SimpleCat::~SimpleCat()

32: {

33: cout << "Simple Cat Destructor...\n";

34: }

35:

36: const SimpleCat & FunctionTwo (const SimpleCat & theCat);

37:

38: int main()

39: {

40: cout << "Making a cat...\n";

41: SimpleCat Frisky;

42: cout << "Frisky is " << Frisky.GetAge() << " years old\n";

43: int age = 5;

44: Frisky.SetAge(age);

45: cout << "Frisky is " << Frisky.GetAge() << " years old\n";

46: cout << "Calling FunctionTwo...\n";

47: FunctionTwo(Frisky);

48: cout << "Frisky is " << Frisky.GetAge() << " years old\n";

49: return 0;

50: }

51:

52: // functionTwo, passes a ref to a const object

53: const SimpleCat & FunctionTwo (const SimpleCat & theCat)

54: {

55: cout << "Function Two. Returning...\n";

56: cout << "Frisky is now " << theCat.GetAge();

57: cout << " years old \n";

58: // theCat.SetAge(8); const!

59: return theCat;

60: }

OUTPUT: 

Making a cat...

Simple Cat constructor...

Frisky is 1 years old

Frisky is 5 years old

Calling FunctionTwo...

FunctionTwo. Returning...

Frisky is now 5 years old

Frisky is 5 years old

Simple Cat Destructor...

ANALYSIS: The output is identical to that produced by Listing 9.11. The only significant change is that FunctionTwo() now takes and returns a reference to a constant object. Once again, working with references is somewhat simpler than working with pointers, and the same savings and efficiency are achieved, as well as the safety provided by using const.

const References

C++ programmers do not usually differentiate between "constant reference to a SimpleCat object" and "reference to a constant SimpleCat object." References themselves can never be reassigned to refer to another object, and so are always constant. If the keyword const is applied to a reference, it is to make the object referred to constant.

When to Use References and When to Use Pointers

C++ programmers strongly prefer references to pointers. References are cleaner and easier to use, and they do a better job of hiding information, as we saw in the previous example.

References cannot be reassigned, however. If you need to point first to one object and then another, you must use a pointer. References cannot be null, so if there is any chance that the object in question may be null, you must not use a reference. You must use a pointer.

An example of the latter concern is the operator new. If new cannot allocate memory on the free store, it returns a null pointer. Since a reference can’t be null, you must not initialize a reference to this memory until you’ve checked that it is not null. The following example shows how to handle this:

int *pInt = new int;

if (pInt != NULL)

int &rInt = *pInt;

In this example a pointer to int, pInt, is declared and initialized with the memory returned by the operator new. The address in pInt is tested, and if it is not null, pInt is dereferenced. The result of dereferencing an int variable is an int object, and rInt is initialized to refer to that object. Thus, rInt becomes an alias to the int returned by the operator new.

DO pass parameters by reference whenever possible. DO return by reference whenever possible. DON’T use pointers if references will work. DO use const to protect references and pointers whenever possible. DON’T return a reference to a local object.

Back to Index