Reference Properties and Oddities

Using reference arguments has several twists you need to know about. First, consider Listing 8.5. It uses two functions to cube an argument. One takes a type double argument, and the other takes a reference to double. The actual code for cubing is purposefully a bit odd to illustrate a point.

Listing 8.5. cubes.cpp


// cubes.cpp -- regular and reference arguments
#include <iostream>
double cube(double a);
double refcube(double &ra);
int main ()
{
    using namespace std;
    double x = 3.0;

    cout << cube(x);
    cout << " = cube of " << x << endl;
    cout << refcube(x);
    cout << " = cube of " << x << endl;
    return 0;
}

double cube(double a)
{
    a *= a * a;
    return a;
}

double refcube(double &ra)
{
    ra *= ra * ra;
    return ra;
}


Here is the output of the program in Listing 8.5:

27 = cube of 3
27 = cube of 27

Note that the refcube() function modifies the value of x in main() and cube() doesn’t, which reminds you why passing by value is the norm. The variable a is local to cube(). It is initialized to the value of x, but changing a has no effect on x. But because refcube() uses a reference argument, the changes it makes to ra are actually made to x. If your intent is that a function use the information passed to it without modifying the information, and if you’re using a reference, you should use a constant reference. Here, for example, you should use const in the function prototype and function header:

double refcube(const double &ra);

If you do this, the compiler generates an error message when it finds code altering the value of ra.

Incidentally, if you need to write a function along the lines of this example (that is, using a basic numeric type), you should use passing by value rather than the more exotic passing by reference. Reference arguments become useful with larger data units, such as structures and classes, as you’ll soon see.

Functions that pass by value, such as the cube() function in Listing 8.5, can use many kinds of actual arguments. For example, all the following calls are valid:

double z = cube(x + 2.0);     // evaluate x + 2.0, pass value
z = cube(8.0);                // pass the value 8.0
int k = 10;
z = cube(k);                  // convert value of k to double, pass value
double yo[3] = { 2.2, 3.3, 4.4};
z = cube (yo[2]);             // pass the value 4.4

Suppose you try similar arguments for a function with a reference parameter. It would seem that passing a reference should be more restrictive. After all, if ra is the alternative name for a variable, then the actual argument should be that variable. Something like the following doesn’t appear to make sense because the expression x + 3.0 is not a variable:

double z = refcube(x + 3.0);  // should not compile

For example, you can’t assign a value to such an expression:

x + 3.0 = 5.0;  // nonsensical

What happens if you try a function call like refcube(x + 3.0)? In contemporary C++, that’s an error, and most compilers will tell you so. Some older ones give you a warning along the following lines:

Warning: Temporary used for parameter 'ra' in call to refcube(double &)

The reason for this milder response is that C++, in its early years, did allow you to pass expressions to a reference variable. In some cases, it still does. What happens is that because x + 3.0 is not a type double variable, the program creates a temporary, nameless variable, initializing it to the value of the expression x + 3.0. Then ra becomes a reference to that temporary variable. Let’s take a closer look at temporary variables and see when they are and are not created.

Temporary Variables, Reference Arguments, and const

C++ can generate a temporary variable if the actual argument doesn’t match a reference argument. Currently, C++ permits this only if the argument is a const reference, but this was not always the case. Let’s look at the cases in which C++ does generate temporary variables and see why the restriction to a const reference makes sense.

First, when is a temporary variable created? Provided that the reference parameter is a const, the compiler generates a temporary variable in two kinds of situations:

• When the actual argument is the correct type but isn’t an lvalue

• When the actual argument is of the wrong type, but it’s of a type that can be converted to the correct type

What is an lvalue? An argument that’s an lvalue is a data object that can be referenced by address. For example, a variable, an array element, a structure member, a reference, and a dereferenced pointer are lvalues. Non-lvalues include literal constants (aside from quoted strings, which are represented by their addresses) and expressions with multiple terms. The term lvalue in C originally meant entities that could appear on the left side of an assignment statement, but that was before the const keyword was introduced. Now both a regular variable and a const variable would be considered lvalues because both can be accessed by address. But the regular variable can be further characterized as being a modifiable lvalue and the const variable as a non-modifiable lvalue.

Now, to return to our example, suppose you redefine refcube() so that it has a constant reference argument:

double refcube(const double &ra)
{
     return ra * ra * ra;
}

Next, consider the following code:

double side = 3.0;
double * pd = &side;
double & rd = side;
long edge = 5L;
double lens[4] = { 2.0, 5.0, 10.0, 12.0};
double c1 = refcube(side);          // ra is side
double c2 = refcube(lens[2]);       // ra is lens[2]
double c3 = refcube(rd);            // ra is rd is side
double c4 = refcube(*pd);           // ra is *pd is side
double c5 = refcube(edge);          // ra is temporary variable
double c6 = refcube(7.0);           // ra is temporary variable
double c7 = refcube(side + 10.0);   // ra is temporary variable

The arguments side, lens[2], rd, and *pd are type double data objects with names, so it is possible to generate a reference for them, and no temporary variables are needed. (Recall that an element of an array behaves like a variable of the same type as the element.) But although edge is a variable, it is of the wrong type. A reference to a double can’t refer to a long. The arguments 7.0 and side + 10.0, on the other hand, are the right type, but they are not named data objects. In each of these cases, the compiler generates a temporary, anonymous variable and makes ra refer to it. These temporary variables last for the duration of the function call, but then the compiler is free to dump them.

So why is this behavior okay for constant references but not otherwise? Recall the swapr() function from Listing 8.4:

void swapr(int & a, int & b)  // use references
{
    int temp;

    temp = a;      // use a, b for values of variables
    a = b;
    b = temp;
}

What would happen if you did the following under the freer rules of early C++?

long a = 3, b = 5;
swapr(a, b);

Here there is a type mismatch, so the compiler would create two temporary int variables, initialize them to 3 and 5, and then swap the contents of the temporary variables, leaving a and b unaltered.

In short, if the intent of a function with reference arguments is to modify variables passed as arguments, situations that create temporary variables thwart that purpose. The solution is to prohibit creating temporary variables in these situations, and that is what the C++ Standard now does. (However, some compilers still, by default, issue warnings instead of error messages, so if you see a warning about temporary variables, don’t ignore it.)

Now think about the refcube() function. Its intent is merely to use passed values, not to modify them, so temporary variables cause no harm and make the function more general in the sorts of arguments it can handle. Therefore, if the declaration states that a reference is const, C++ generates temporary variables when necessary. In essence, a C++ function with a const reference formal argument and a nonmatching actual argument mimics the traditional passing by value behavior, guaranteeing that the original data is unaltered and using a temporary variable to hold the value.


Note

If a function call argument isn’t an lvalue or does not match the type of the corresponding const reference parameter, C++ creates an anonymous variable of the correct type, assigns the value of the function call argument to the anonymous variable, and has the parameter refer to that variable.


C++11 introduces a second kind of reference, called an rvalue reference, that can refer to an rvalue. It’s declared using &&:

double && rref = std::sqrt(36.00);  // not allowed for double &
double j = 15.0;
double && jref = 2.0* j + 18.5;     // not allowed for double &
std::cout << rref << ' ';          // display 6.0
std::cout << jref << ' ';          // display 48.5;

The rvalue reference was introduced mainly to help library designers provide more efficient implementations of certain operations. Chapter 18, “Visiting will the New C++ Standard,” discusses how rvalue references are used to implement an approach called move semantics. The original reference type (the one declared using a single &) is now called an lvalue reference.

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

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