Design Con 2015

Reference Initializations

August 31, 2001

Dan_Saks-August 31, 2001

Reference Initializations

The rules for initializing references resemble the rules for initializing pointers. But they do differ. And you should know how.

I began this year's columns with a series on reference types in C++. (See "An Introduction to References," January 2001, p. 81 for the first of these.) I then realized I needed to employ the concepts of lvalues and rvalues in further discussions of references, so I took a little detour to explain them. (See "Lvalues and Rvalues," June 2001, p. 70.) Now I'm returning to my explanation of references with a look at one aspect of reference initialization that many programmers find somewhat surprising.

Back to references

References in C++ provide many of the same capabilities as pointers. A reference, like a pointer, is an object that you can use to refer indirectly to another object. The difference between pointers and references is that you must use an explicit operator-the * operator-to dereference a pointer, but you don't use an operator to dereference a reference. A reference automatically dereferences when you access it. For example, if pt is a "pointer to T" pointing to object x of type T, expression *pt derefences pt to refer to x. In contrast, if rt is a "reference to T" referring to x, expression rt-without any operators at all-dereferences rt to refer to x.

A reference is essentially a const pointer (not pointer to const!) that's automatically dereferenced each time it's used. You can always rewrite code that uses references as code that uses const pointers. For example, a reference declaration such as:

int &ri = i;

is equivalent to a pointer declaration such as:

int *const pi = &i;

An assignment to the reference, as in:

ri = 4;

is equivalent to an assignment to the explicitly dereferenced pointer, as in:

*pi = 4;

A reference is also equivalent to a const pointer in that:

  • Once you create it, you can't change it to refer to something else.
  • Since you can't change it after you create it, you must give it a value at the time you create it.

When pointer pi has a null value, evaluating *pi is an invalid operation. (Strictly speaking, it has undefined behavior.) When it has a non-null value, pi points to an object and *pi is a valid expression referring to that object. Thus, *pi is an lvalue-an expression that designates an object. So is the reference expression ri.

Initializing pointers and references

The rules for initializing references closely resemble the rules for initializing pointers. At the moment, I'm interested in one particular rule regarding pointers: a variable of type "pointer to T" can be initialized to point only to an lvalue of type T. The corresponding rule for references is: a variable of type "reference to T" can be initialized to refer only to an lvalue of type T.

For example, given:

int i;

then:

double *pd = &i; // error

is an error because a pointer to double can't point to an object of type int. Similarly:

double &rd = i; // error

is an error because a reference to double can't bind to an object of type int.

Although 3.0 is a double, a declaration such as:

double *pd = &3.0; // error

is an error because 3.0 is an rvalue, not an lvalue, and you can't take the address of an rvalue. By the same token:

double &rd = 3.0; // error

is also an error. You can't bind a reference to an rvalue.

In all honesty, the rules stated here are too simple to cover initialization of pointers or references to cv-qualified (const- or volatile-qualified) types. My previous statement of the reference initialization rule suggests that the type of the reference must be exactly the same as the type of the initializing expression. In fact, the type may have different cv-qualifiers, provided that every qualifier in the type of the initializing expression is also in the type of the reference.

For example, given:

int i;

then:

int const &rci = i; // OK

is quite proper, as are both:

int volatile &rvi = i; // OK

int const volatile &rcvi = i; // OK

In each case, the reference refers to a type with more cv-qualifiers than the initializing expression.

On the other hand, given:

int const ci = -5309;

then:

int &ri = ci; // error

is an error because ri refers to a type, int, that lacks the cv-qualifier found in its initializing expression, which has type const int. A declaration such as:

int volatile &rvi = ci; // error

is an error for the same reason.

Before I provide a better statement of the rules for initializing pointers and references, I need to formalize the notion of one type having more cv-qualifiers than another. A cv-qualified type has the form "cv T" where cv is a sequence of cv-qualifiers and T is a type (without cv-qualifiers). The sequence cv can be empty, just const by itself, just volatile by itself, or const volatile (in either order). Now here's the formal notion: for any two sequences of cv-qualifiers cv1 and cv2, we say that cv1 has the same or greater cv-qualification than cv2, and write cv1 >= cv2, if every cv-qualifier in cv2 also appears in cv1.

Here, then, is a better statement of the rules for initializing pointers and references:

  • A variable of type "pointer to cv1 T" can be initialized to point only to an lvalue of type "cv2 T," where cv1 >= cv2.
  • A variable of type "reference to cv1 T" can be initialized to refer only to an lvalue of type "cv2 T," where cv1 >= cv2.

For example, let's apply the rule for reference initialization just above to:

int const &rci = i; // OK

where i is a variable of type int. Matching this declaration to the rule, cv1 is the sequence of cv-qualifiers in the type of rci and cv2 is the sequence of cv-qualifiers in the type of i. In that case, cv1 is the sequence const and cv2 is the empty sequence. Clearly, cv1 >= cv2 is true, and this is a valid reference initialization.

On the other hand, in:

int volatile &rvi = ci; // error

where ci is a variable of type const int, cv1 is the sequence volatile and cv2 is the sequence const. In this case, cv1 >= cv2 is not true, and this initialization produces a compile-time error.

In the case where both sequences, cv1 and cv2, are empty, as in:

int &ri = i; // OK

it's still true that cv1 >= cv2.

This rule for pointers is still too simple. For example, it doesn't cover a declaration such as:

void *p = &i; // OK

nor does it cover initializing a pointer using a derived-class-to-base-class conversion. However, these rules are sufficiently accurate as a foundation for the following discussion.

Initializing references-to-const

There is an exception to the rule for initializing references: a variable of type "reference to const T" can be initialized with an expression e that is not an lvalue of type T, provided there's a conversion from e's type to T. In other words, when initializing a "reference to const T", the initializing expression e might be an rvalue of type T, or an expression (lvalue or rvalue) of type U different from T, where there's a conversion from U to T. In all cases, the program initializes the reference by:

1. Creating storage for a temporary object of type T (so that the reference has something to bind to)

2. Converting e to T, if necessary, and placing the result in the temporary storage

3. Binding the reference to the temporary storage

The lifetime of the temporary is the same as that of the reference. When the program no longer needs the reference (such as when the reference goes out of scope), the program discards the temporary as well.

Whereas a pointer initialization such as:

double const *pcd = &1;

is an error, a reference initialization such as:

double const &rcd = 1;

is valid. In this case, the compiler generates code to create storage for a temporary object of type double, convert 1 from int to double, place the result in the temporary, and bind rd to the temporary.

Since 1 is a constant, any decent optimizing compiler can convert 1 (an int) to 1.0 (a double) at compile time. However, that optimization is not available when the initializing expression is not constant, as in:

double const &rcd = n;

where n is a variable of type int. In this case, the compiler has no choice but to generate code that will convert n to double at run time.

The program does not generate a temporary object during reference binding unless it has to. For example, in:

int const &rci = i; // OK

the program binds rci directly to int variable n. There's no need to create a temporary to hold a copy of n.

Okay, why?

I expect that many of you are wondering why C++ initializes references-to-const in this special way. Well, I'll tell you, but not until next time.

Dan Saks is the president of Saks & Associates, a C/C++ training and consulting company. He is also a consulting editor for the C/C++ Users Journal. You can write to him at dsaks@wittenberg.edu.

Return to September 2001 Table of Contents

Loading comments...

Parts Search Datasheets.com

KNOWLEDGE CENTER