A surprising number of C++ programmers still don't understand or properly use const. If you are one of them, let Dan gently guide you to const correctness.
Copyright © 1996 by Dan Saks
As I hope at least some of you have noticed, I try to use the const qualifier very conscientiously in my code. It's my way of encouraging others to do the same. Adding the const qualifier at the appropriate places in your code gives the compiler more information about how you intend to handle your data. The compiler can then use these insights to protect you from some of your own gaffes.
Beyond just trying to set a good example, I have on occasions written specifically about the const qualifier in C++. I believe I first discussed it, and const member functions, in "Stepping Up to C++: Dynamic Arrays," CUJ, November 1992. I discussed them again when I explained the new mutable keyword in "Stepping Up to C++: Mutable Class Members," CUJ, April 1995. Each of those times, I put in a plug for the hygienic benefits of using const. I explained the new rules for conversions involving pointers to const-qualified types in "Stepping Up to C++: More Minor Enhancements as of CD Registration," CUJ, March 1995. I also said a few more words about const in "C++ Theory and Practice: Perspectives on Grammars and Parsers," CUJ, May 1996, but that time I believe I refrained from proselytizing.
Until recently, I had the impression that gentle nudging was all that many C++ programmers needed to hop on the const wagon. But over the past year, I've been querying my lecture audiences a little more deeply about const, and found that at least half of them apparently have some fundamental misunderstanding about the nature of constness in C++ and C. So I feel moved to write a few words to help alleviate that confusion.
Pop Quiz, Hotshot
I begin, as I usually do in my lectures, with the following simple question. Given
char *p; const char *pc;
then one of the following assignments is valid (will compile as both C++ and C), and one is not:
p = pc; // (1) pc = p; // (2)
Which is which?
I repeatedly find that about half the audience will hesitatingly say (1) is valid, and the other half will hesitatingly say (2) is valid. A few may suggest that neither or both should compile.
The answer is in fact that (2) compiles while (1) does not. I believe the key to understanding the answer to this and more complicated questions about constness comes when you learn to think of const as a promise.
The Nature of the Promise
declares p with type "pointer to char". The absence of a const qualifier anywhere in the declaration means that the program can change p to point to different characters, and it can change the value of any of those characters, using expressions such as:
*p++ = '\0'; // OK
const char *pc;
declares pc with type "pointer to const char". Consequently, the expression *pc has type "const char", as do expressions such as *(pc + i) and pc[i]. The pointer is not const, so it can point to different character objects, but any object that it points to is const. That is, a program can change the value of pc, but not the value of *pc:
++pc; // OK *pc = '\0'; // no
In a sense, pc's declaration is a promise that, although the program can use pc to reach different characters, it will never use pc to change any of those characters. The compiler keeps the program honest by rejecting any expression (such as the one just above) that tries to break the promise.
Clearly, a program that declares p and pc as above can also declare
char *q; const char *qc;
q = p; // OK qc = pc; // OK
qc has exactly the same type as pc, so qc is under the same obligation as pc to look but not touch. Just as a compiler will reject an attempt to alter *pc, such as:
*pc = '\0'; // no
it will also reject attempts to alter *qc, as in:
*qc = '\0'; // no
Therefore, assigning pc to qc does not compromise the program's integrity.
Even though qc and p do not have exactly the same type, the assignment
qc = p;
is nonetheless perfectly safe. Let's analyze this in terms of promises made and kept.
Heading into the assignment, p points to some character data, possibly one character in an array of characters. The compiler regards p as the keeper of that data. However, p's declaration makes no promises at all, so the program can freely write to *p (whatever p points to). On the other hand, qc's declaration promises that the program will not write to *qc (whatever qc points to). To some, these promises seem contradictory. After all, how can a program promise that *qc won't change when p points to the same object as qc and *p might change?
The reason there's no contradiction is that the const promise is not all that strong. In C++, as in C, the declaration
const char *qc;
does not promise that the character(s) that qc points to will never change. It merely promises that the program won't use qc to change any of those characters.
Thus, assigning p (a char *) to qc (a const char *) does not violate any promises made in the program. The program can use p to change some characters. Copying a value from p to qc does not impose any restrictions on what the program can do with p or *p. It can still use p to scribble wherever it wants. The only assurance is that, if any character value changes, it won't be at the hands of qc.
qc = p; // OK
does not violate any promises,
p = qc; // no
does. Here, the program regards qc as the original keeper of the character data. The declaration for qc promises that the program will not use qc to alter any of those characters, and so the program entrusts qc with preventing changes to that data.
Assigning qc to p effectively hands control of qc's data to p. But the declaration for p makes no promises about protecting the value of that data, and so p cannot be trusted. If the program were to allow the assignment from qc to p, then a later assignment such as
*p = '\0';
would clobber a character that qc had sworn to protect. Although the real transgression doesn't occur until the assignment to *p, it's not p's fault. p is only doing what it warned it might do. Rather, the original sin is in the assignment from qc to p. Consequently, C and C++ do not allow that assignment. They do not let qc weasel out of its promise by handing data over to p while knowing full well that p is untrustworthy.
qc = p;
converts a char * to a const char *. This is just one of a variety of conversions that the draft C++ Standard calls "qualification conversions." The qualification conversion rule for pointers is:
An rvalue of type "pointer to cv1 T" can be converted to an rvalue of type "pointer to cv2 T" if "cv2 T" is more cv- qualified than "cv1 T."
const is one of two cv-qualifiers, the other being volatile. Almost all the conversion rules that apply to const apply to volatile as well. In the rule above, cv1 and cv2 each represent a possibly empty combination of const and/or volatile.
Elsewhere the draft defines what it means for cv2 T to be "more cv-qualified" that cv1 T. Actually, it defines "less cv-qualified" as the following partial ordering:
no cv-qualifier < const no cv-qualifier < volatile no cv-qualifier < const volatile const < const volatile volatile < const volatile
and leaves it to you to infer the meaning of "more cv-qualified." It means what you think it means.
For example, given
char *p; const char *pc; volatile char *pv; const volatile char *pcv;
then p is less cv-qualified than every other, and pcv is more cv-qualified than every other. Thus, you can copy any of those pointers to pcv, yet none of them to p (except p itself). pc is neither more nor less cv-qualified than pv, so you can't copy from one to the other (in either direction).
The qualification conversion rule appears to disallow:
const char *pc, *qc; ... pc = qc;
because the destination, pc, is no more cv-qualified than the source, qc. Since pc and qc have exactly the same type, the assignment requires no conversion whatsoever. A blanket statement in the draft says that a valid conversion includes no conversion at all.
The draft also has rules that allow binding a "reference to cv1 T" to an lvalue (an object) of type "cv2 T", provided that cv1 is no less cv-qualified than cv2. For example,
T x; const T &r = x; // OK
is valid because r is no less cv-qualified than x.
These conversion rules apply not only in assignments and reference declarations, but in parameter passing as well. For example, if you declare
size_t strlen(const char *s); ... char name = "Ben";
the call strlen(name) converts name from type "array of char" to type "pointer to char", which it then converts by a qualification conversion, to "pointer to const char".
Logically const vs. Physically const
Since a program can convert a T * into const T * (or bind a const T & to a T), it means that a given const-qualified expression may designate an object that is non-const elsewhere in the program.
For example, the call strlen(name) above initializes strlen's parameter, s, with the address of the first character in array name. The actual argument name is not const, but the declaration for parameter s promises nonetheless to treat all the characters as if they were. This is as it should be. strlen need not alter the array contents as it computes the length. The const qualifier in the declaration for s gives the compiler the ability to detect accidental assignments to *s. *s is a "logically const" expression which, during this particular call, designates a non-const object.
During some other call, *s might designate an object that is physically const. A "physically const" object is one that truly immutable or unchangeable, and therefore may reside in a read-only data region. For example,
static const char title = "Mr.";
defines title as a const object with static storage duration that can be initialized during translation. It is physically const, and a translator is free to place it in ROM. During the call strlen(title), *s is a logically const expression that refers to a physically const object.
const in Practice
Physically const objects are useful, particularly in embedded applications that must place data in ROM. However, I find that I use const mostly for declaring parameters that are pointers or references to logically const objects.
In general, when writing a function with a parameter that is a pointer or reference to an object, you should ask yourself if the function ever needs to alter that object. If the answer is no, then the object is logically const, and you should const-qualify the type to which the pointer or reference refers. This lets you enlist the compiler's help in enforcing the parameter's logical constness.
Moreover, if you don't declare the parameter as const, then the program can't pass a physically const object to the function, even though it should be safe to do so. For example, suppose strlen's parameter had been declared non-const, as in:
size_t strlen(char *s);
Then a program with physically const data, such as:
static const char title = "Mr.";
couldn't call strlen(title), because parameter s would be less cv-qualified than argument title.
When declaring a pointer parameter, you can use const in several different places. Not only can you declare a parameter p as
const T *p // (1)
you can also declare it as
T const *p // (2) T *const p // (3) const T *const p // (4) T const *const p // (5)
Let's sort through them.
(2) is equivalent to (1). const and T are both decl-specifiers, and the order in which they appear is unimportant. Both declare p with type "pointer to const T". Either declaration for parameter p is a clear statement from the callee to the caller "you can pass me the address of your data, and I may wander all around looking at it, but you can rest assured that I won't change it." As I explained in the earlier discussion, this is quite useful for parameter passing. It imposes a meaningful restriction on the callee's behavior.
(3) is quite a bit different from (1) and (2). Here, const is part of the declarator. It modifies the pointer, not what the pointer points to. It declares p with type "const pointer to T". In effect, the callee says to the caller "you can pass me the address of your data, and I promise I won't change my copy of that address; however, I may use that address to scrawl on your data." The caller's reaction is likely "Gee, thanks for nothing. I don't care what you do with your copy of my data's address, but I do care about what you do with my data."
The fact is, (3) makes no promises about how the callee will treat the caller's data. Even though p itself is const, what it points to is not. The callee can easily copy p to another pointer, declared without any const qualifier:
char *q = p;
The callee can make this copy without changing p and without violating any promises regarding what p points to (because there are none). The callee can then use q to scribble all over the caller's data. For the caller's perspective, the const in (3) is meaningless.
(4) and (5) are equivalent. Again, the relative order of const and T is unimportant. (4) is a combination of (1) and (3). The part that corresponds to (1) is useful, but the part that corresponds to (3) is not. Therefore, I see no reason to use (4) in preference to (1).
The previous discussion does not apply to reference parameters. Declarations such as
T &const r const T &const r
are syntactically incorrect. const cannot appear in a declarator immediately after the & that declares a reference.
What about using the const qualifier in parameters passed by value? For example, is the const in
void f(const int n)
useful? I think not. This declaration for f tells the caller "I will make a copy of the int you pass me, and I promise I won't change that copy." Big whoop.
Declaring a parameter passed by value as const has no impact on the callee's interaction with its caller. It merely imposes a restriction on the callee's implementation. This is very analogous to declaring a pointer parameter as T *const p. In both cases, the const in the parameter declaration conveys implementation rather than behavior, which is inappropriate.
I have presented this discussion at a number of the semi-annual Software Development conferences. Once (just once), someone in the audience took exception when I advised against declaring by-value parameters as const. My recollection is that she claimed that she used an architecture in which the compiler could take advantage of the const parameter to generate better (faster or tighter) code than it could if the parameter were non-const. I believe such architectures are rare, if they exist at all, but I remain open to the possibility.
Dan Saks is the president of Saks &Associates, which offers consulting and training in C++ and C. He is secretary of the ANSI and ISO C++ committees. Dan is coauthor of C++ Programming Guidelines, and codeveloper of the Plum Hall Validation Suite for C++ (both with Thomas Plum). You can reach him at 393 Leander Dr., Springfield OH, 45504-4906, by phone at (513)324-3601 (the area code changes to 937 after September 1996), or electronically at email@example.com.