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.