1. 程式人生 > >References and Const

References and Const

Just as you can use the const qualifier in pointer declarations, you can also use it in reference declarations-with one notable exception.

Last month, I explained the basic capabilities of reference types in C++.[1] A reference is an object that refers indirectly to another object. They offer many of the same capabilities as pointers. The key difference between pointers and references is their appearance when you use them. Whereas you must explicitly use an operator, such as *

or [], to dereference a pointer, you don't have to do anything special to dereference a reference. A reference dereferences itself when you use it. Just as you can use the const qualifier in pointer declarations, you can also use it in reference declarations, with one notable exception. With a pointer declaration, you can declare either a "pointer to const
" or a "const pointer." With a reference declaration, you can declare only a "reference to const." You can't declare a "const reference," at least, not directly. Here's why this is so.

 I'll begin with a look at the syntax of reference declarations, which is similar, but not identical, to the structure of pointer declarations. I described the structure of C and C++ declarations in some detail in an earlier column, so I won't repeat it all now.

[2] Here are the key points as they relate to references.

Every C and C++ declaration has two principal parts: a sequence of zero or more declaration specifiers, and a sequence of one or more declarators, separated by commas. For example:

A declarator is the name being declared, possibly surrounded by operators such as *, &, [], and (). The * operator in a declarator means "pointer to," & means "reference to," and [] means "array of."

The operators in a declarator group according to the same precedence as they do when they appear in an expression. For example, [] has higher precedence than *. Thus the declarator *x[N] from the previous example means that x is an "array with N elements of type pointer" rather than a "pointer to an array with N elements."

Parentheses serve two roles in declarators: as the function call operator or as grouping. As the function call operator, () has the same precedence as []. As grouping, () trumps all other operators. For example, in:

char *f(int);

the ()s around the parameter list have higher precedence than the *. Thus, the f declared just above is a "function returning a pointer to a char" rather than a "pointer to a function returning a char." If the latter is what you want, then:

char (*f)(int);

is what you must write.

The & operator has the same precedence as *. Thus:

char &g(int);

declares g as a "function returning a reference to a char" rather than as a "reference to a function returning a char." If what you want is a "reference to a function ...," then you must write the declaration as:

char (&g)(int);

A style point

Many-probably most-C++ programmers lay out reference declarations with the & operator adjacent to the last declaration specifier, rather than as part of the declarator. For example, they write declarations such as:

char& g(int);

I prefer to keep the & with the declarator, as in:

char &g(int);

I believe that the latter form more accurately reflects the syntactic structure of the declaration. Declarations are among the most confusing parts of C++. Separating the & from the declarator will often add to the confusion.

Reference to const

The declaration specifiers leading up to a declarator can be type specifiers such as int, unsigned, or an identifier that names a type. They can be storage class specifiers such as extern or static. They can also be function specifiers such as inline or virtual.

When it appears as a declaration specifier, the keyword const is a type specifier. For example, the const in:

int const &ri = n;

modifies int, the type of the object to which ri refers. This declaration declares that ri is a "reference to a const int" referring to n.

When you use ri in an expression, it behaves as an object of type "const int." This means you can use ri to read but not modify the int to which it refers. For example, expressions such as:

ri = 0; // error

++ri; // error

won't compile because they attempt to modify the object to which ri refers.

In this particular example, ri refers to n. Although you can't use ri to modify n, you might still be able to modify n by means of some other expression. It all depends on how you declare n. If you declare n as:

int const n = 255;

then n is indeed a non-modifiable object, and you can't alter n by any means (other than by using a cast expression). On the other hand, if you declare n as:

int n = 255;

then n is a modifiable object. You can modify n using expressions such as:

n = 2 * n;

++n;

You can't perform these operations using ri because ri is const.

In general, for any type T, an object of type "reference to const T" can refer to an object of type plain T as well as an object of type "const T." In either event, the compiler treats the reference as if it refers to a const object. C++ treats a "pointer to const T" in essentially the same way. An object of type "pointer to const T" can point to an object of type plain T as well as an object of type "const T." In either event, the compiler treats the pointer as if it points to a const object.[3]

Another style point

The order in which the declaration specifiers appear in a declaration doesn't matter to the compiler. Thus, for example:

const int &ri = i; (1)

is equivalent to:

int const &ri = i; (2)

Once again, I find myself swimming against the tide. As I explained a couple of years ago, the vast majority of C++ programmers prefer to write const to the left of the other type specifiers, as in (1).[2] I prefer to write const to the right, as in (2). I have found over the years that teaching students to write const as the rightmost type specifier in pointer declarations helps them better understand the const qualifier's effect. I write reference declarations in the same style as pointer declarations just to be consistent.

const references

As I mentioned earlier, pointer declarations let you declare either a "pointer to const" or a "const pointer." For example:

int const *p = &i;

declares p as an object of type "pointer to const int, whereas:

int *const q = &j;

declares q as an object of type "const pointer to int." In the latter declaration, the key word const appears in the declarator. Specifically, it's part of a syntactic unit called a ptr-operator. A ptr-operator is either a * by itself or a * followed immediately by the key word const.

Of course, a ptr-operator can also be an & operator, as in:

int &rj = j;

However, it cannot be an & operator followed by const. That is:

int &const rj = j; // error

is a syntax error. In short, while you can declare a "reference to const," you can't declare a "const reference." The grammar just doesn't allow it.

The grammar doesn't allow you to declare a "const reference" because a reference is inherently const. Once you bind a reference to refer to an object, you cannot bind it to refer to a different object. There's no notation for rebinding a reference after you've declared the reference. For example:

int &ri = i;

binds ri to refer to i. Then an assignment such as:

ri = j;

doesn't bind ri to j. It assigns the value in j to the object referenced by ri, namely, i.

Once you define a reference, you can't change it to refer to something else. Since you can't change the reference after you define it, you must bind the reference to an object at the beginning of its lifetime. For example, a reference declaration at block scope must have an initializer, as in:            
void f()
{
int &ri = i;
...
}

Omitting the initializer is an error:            
void f()
{
int &ri;  // error
...
}

Although you can't define a "const reference"directly, you can do it indirectly through a typedef. For example, 

           
typedef int &ref_to_int;
...
int_ref const r = i;

defines r as a "const int_ref." Since int_ref is just an alias for "reference to int," r's type appears to be "const reference to int." But a reference is inherently const, so the const here is redundant and it has no effect. C++ just ignores the const in this declaration, so r has type "reference to int."

A word about terminology

Whereas I write declarations such as:

int const &ri = i;

most C++ programmers would write it as:

const int& ri = i;

I can live with that. What bothers me more is that most C++ programmers would also call ri a "const reference." It's really a "reference to const."

As I explained previously, C++ does not allow you to declare a "const reference." Since you can't have a "const reference," the only thing that "const reference" could be is a shorter way of saying "reference to const." Right? So what's my problem?

My problem is that while there may be no reason to distinguish "reference to const" from "const reference," there is certainly a reason to distinguish "pointer to const" from "const pointer." A "pointer to const" is not the same as a "const pointer." The difference is significant.

I've noticed that programmers who say "const reference" when they mean "reference to const" tend to say "const pointer" when they mean "pointer to const." In fact, many use the term "const pointer" to mean either "const pointer" or "pointer to const." I find this confusing. I suspect others do, too.

I encourage you to avoid using the term "const reference" when what you really mean is "reference to const."

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. He served for many years as secretary of the C++ standards committee. With Thomas Plum, he wrote C++ Programming Guidelines. You can write to him at [email protected].

Saks, Dan, "An Introduction to References," Embedded Systems Programming, January 2001, p. 81.
Back
Saks, Dan, "const T vs. T const," Embedded Systems Programming, February 1999, p. 13.
Back Saks, Dan , "What const Really Means," Embedded Systems Programming, August 1998, page 11.
Back