6.13 References and Reference Parameters

Two ways to pass arguments to functions in many programming languages are pass-by-value and pass-by-reference. When an argument is passed by value, a copy of the argument’s value is made and passed (on the function-call stack) to the called function. Changes to the copy do not affect the original variable’s value in the caller. This prevents the accidental side effects that so greatly hinder the development of correct and reliable software systems. So far, each argument in the book has been passed by value.

Performance Tip 6.2

One disadvantage of pass-by-value is that, if a large data item is being passed, copying that data can take a considerable amount of execution time and memory space.

Reference Parameters

This section introduces reference parameters—the first of the two means C++ provides for performing pass-by-reference. With pass-by-reference, the caller gives the called function the ability to access the caller’s data directly, and to modify that data.

Performance Tip 6.3

Pass-by-reference is good for performance reasons, because it can eliminate the pass-by-value overhead of copying large amounts of data.

 

Software Engineering Observation 6.9

Pass-by-reference can weaken security; the called function can corrupt the caller’s data.

After this section’s example, we’ll show how to achieve the performance advantage of pass-by-reference while simultaneously achieving the software engineering advantage of protecting the caller’s data from corruption.

A reference parameter is an alias for its corresponding argument in a function call. To indicate that a function parameter is passed by reference, simply follow the parameter’s type in the function prototype by an ampersand (&); use the same convention when listing the parameter’s type in the function header. For example, the parameter declaration


int& number

when read from right to left is pronounced “number is a reference to an int.” In the function call, simply mention the variable by name (e.g., number) to pass it by reference. In the called function’s body, the reference parameter actually refers to the original variable in the calling function, and the original variable can be modified directly by the called function. As always, the function prototype and header must agree. Note the placement of & in the preceding declaration—some C++ programmers prefer to write the equivalent form int &number.4

Passing Arguments by Value and by Reference

Figure 6.17 compares pass-by-value and pass-by-reference with reference parameters. The “styles” of the arguments in the calls to function squareByValue and function squareByReference are identical—both variables are simply mentioned by name in the function calls. Without checking the function prototypes or function definitions, it isn’t possible to tell from the calls alone whether either function can modify its arguments. Because function prototypes are mandatory, the compiler has no trouble resolving the ambiguity. Chapter 8 discusses pointers, which enable an alternate form of pass-by-reference in which the style of the function call clearly indicates pass-by-reference (and the potential for modifying the caller’s arguments).

Common Programming Error 6.7

Because reference parameters are mentioned only by name in the body of the called function, you might inadvertently treat reference parameters as pass-by-value parameters. This can cause unexpected side effects if the original variables are changed by the function.

Fig. 6.17 Passing arguments by value and by reference.

Alternate View

 1   // Fig. 6.17: fig06_17cpp
 2   // Passing arguments by value and by reference.
 3   #include <iostream>
 4   using namespace std;
 5
 6   int squareByValue(int); // function prototype (value pass)          
 7   void squareByReference(int&); // function prototype (reference pass)
 8
 9   int main() {
10      int x{2}; // value to square using squareByValue
11      int z{4}; // value to square using squareByReference
12
13      // demonstrate squareByValue
14      cout << "x = " << x << " before squareByValue
";
15      cout << "Value returned by squareByValue: "
16         << squareByValue(x) << endl;
17      cout << "x = " << x << " after squareByValue
" << endl;
18
19      // demonstrate squareByReference
20      cout << "z = " << z << " before squareByReference" << endl;
21      squareByReference(z);
22      cout << "z = " << z << " after squareByReference" << endl;
23   }
24
25   // squareByValue multiplies number by itself, stores the
26   // result in number and returns the new value of number
27   int squareByValue(int number) {                              
28      return number *= number; // caller's argument not modified
29   }                                                            
30
31   // squareByReference multiplies numberRef by itself and stores the result
32   // in the variable to which numberRef refers in function main
33   void squareByReference(int& numberRef) {                
34      numberRef *= numberRef; // caller's argument modified
35   }                                                       

x = 2 before squareByValue
Value returned by squareByValue: 4
x = 2 after squareByValue

z = 4 before squareByReference
z = 16 after squareByReference

References as Aliases within a Function

References can also be used as aliases for other variables within a function (although they typically are used with functions as shown in Fig. 6.17). For example, the code


int count{1}; // declare integer variable count
int& cRef{count}; // create cRef as an alias for count
++cRef; // increment count (using its alias cRef)

increments variable count by using its alias cRef. Reference variables must be initialized in their declarations and cannot be reassigned as aliases to other variables. In this sense, references are constant. All operations supposedly performed on the alias (i.e., the reference) are actually performed on the original variable. The alias is simply another name for the original variable. Unless it’s a reference to a constant (discussed below), a reference’s initializer must be an lvalue (e.g., a variable name), not a constant or rvalue expression (e.g., the result of a calculation).

const References

To specify that a reference parameter should not be allowed to modify the corresponding argument, place the const qualifier before the type name in the parameter’s declaration. For example, consider class Account (Fig. 3.2). For simplicity in that early example, we used pass-by-value in the setName member function, which was defined with the header


void setName(std::string accountName)

When this member function was called, it received a copy of its string argument. string objects can be large, so this copy operation degrades an application’s performance.

For this reason, string objects (and objects in general) should be passed to functions by reference. Class Account’s setName member function does not need to modify its argument, so following the principle of least privilege, we’d declare the parameter as


const std::string& accountName

Read from right to left, the accountName parameter is a reference to a string constant. We get the performance of passing the string by reference, but setName treats the argument as a constant, so it cannot modify the value in the caller—just like with pass-by-value. Code that calls setName would still pass the string argument exactly as we did in Fig. 3.1.

Performance Tip 6.4

For passing large objects, use a const reference parameter to simulate the appearance and security of pass-by-value and avoid the overhead of passing a copy of the large object.

Similarly, class Account’s getName member function was defined with the header


std::string getName() const

which indicates that a string is returned by value. Changing this to


const std::string& getName() const

indicates that the string should be returned by reference (eliminating the overhead of copying a string) and that the caller cannot modify the returned string.

Returning a Reference to a Local Variable

Functions can return references to local variables, but this can be dangerous. When returning a reference to a local variable—unless that variable is declared static—the reference refers to a variable that’s discarded when the function terminates. An attempt to access such a variable yields undefined behavior. References to undefined variables are called dangling references.

Common Programming Error 6.8

Returning a reference to a local variable in a called function is a logic error for which compilers typically issue a warning. Compilation warnings indicate potential problems, so most software-engineering teams have policies requiring code to compile without warnings.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset