Volatile as a promise
Last time around, I advised you to place the volatile qualifier where it most accurately models the behavior of your hardware.1 In justifying that advice, I mentioned that for any type T, both C and C++ provide a standard (built-in) conversion from "pointer to T" to "pointer to volatile T," yet prohibit conversion in the other direction. At the time, I also hinted at the similarity between the conversion rules involving "pointer to volatile" and those involving "pointer to const." I think that similarity deserves exploration.
The conversion rules involving "pointer to volatile" are nearly identical to those involving "pointer to const," so once you understand one set, you pretty much understand the other. The operative word here is understand, rather than just remember. You can mix const and volatile with types in a great many ways, so it's too hard for most of us to remember all the different conversion scenarios. Thus, the key to success is to acquire insights into the conversion rules so you can reason each situation out when necessary.
I presented some of those insights in columns I wrote way back in the last millennium.2, 3 However, those columns focused entirely on the const qualifier and said nothing about volatile. This month, I'll review conversions involving const and extend them to conversions involving volatile.
const as a promise
C and C++ permit certain conversions between similar pointer types that differ only in their use of const. For example, given:
T *p; ... void f(T const *qc);
a program can call f(p). In passing p to f, the program converts p's value from "pointer to T" into "pointer to const T." This is a valid conversion.
In contrast, given:
T const *pc; ... void g(T *q);
calling g(pc) produces a compile-time error. The compiler rejects the attempt to convert pc's value from "pointer to const T" into "pointer to T."
It's easy to see the rationale for these conversion rules once you start to think of const as a promise (presuming that you have a sense of ethics). In essence the conversion rules are based in a simple ethical principle: you need not make any promises, but once you make one, you must keep it. Here's how it applies.
The presence of const in a declaration such as:
int const *p;
is effectively a promise that the program will not use a value obtained directly or indirectly from p to modify any int objects. The compiler enforces the promise by rejecting any operation that attempts to modify an object accessed as *p, as in:
*p += 3; // error ++(*p); // error
Neither expression will compile.
The declaration for p is not a promise that p will point only to constant objects. In fact, p can point to either const or nonconst objects. It's just that p promises to treat any objects it might point to as if they all were constant.
For example, the Standard C function strlen is declared as:
size_t strlen(char const *s);
In effect, strlen promises not to alter the characters of any array passed to it. Calling strlen(buf), where buf is some character array, doesn't promise that the characters in buf won't change. It only promises that strlen won't be the one to change them.
Once you understand const as a promise, it's easy to see why, given:
T const *pc; ... void g(T *q);
you can't call g(pc). The declaration of pc promises that the program won't use the value in pc to modify any T objects. However, the declaration for g's parameter q makes no such promise. This doesn't mean that function g will necessarily change the value that q points to, but it does mean that g is leaving that option open. Therefore, the compiler can't entrust q with a copy of pc, because g might then use q to violate the promise in pc's declaration.
On the other hand, given:
T *p; ... void f(T const *qc);
then calling f(p) poses no problem. In this case, p's declaration makes no promises, so calling f(p) can't violate any promise. However, the declaration for f's parameter qc makes a promise that f won't use the pointer value in qc to modify any T objects. The compiler will enforce this promise when it compiles the body of f. This means, for example, that f must not call g(qc), because g's parameter q doesn't keep the promise.
A different promise, but a promise nonetheless
The keyword volatile can appear anywhere that the keyword const can appear, either in addition to or in place of const. The C standard refers to const and volatile collectively as type qualifiers. The C++ standard calls them cv-qualifiers. I do, too.
Whereas a const object is one whose value the program can't change, a volatile object is one whose value might change spontaneously. That is, when you declare an object to be volatile, you're telling the compiler that the object might change state even though no statements in the program appear to change it. As I explained in my previous column, volatile is useful in declaring objects representing memory-mapped device registers, as in:1
UART volatile *const UART0 = (UART *)0x03FFD000;
In this case, UART0 is a const pointer to a volatile UART object. That UART might change state because of actions performed by the program, but it might also change state in response to the behavior of external hardware.
Most processors can perform operations faster when the operands reside in CPU registers rather than in ordinary memory. Compilers can optimize operations on nonvolatile objects by reading an object's value into a CPU register, working with that register for a while, and eventually writing the value in the register back to the object. Compilers can't do this sort of optimization with volatile objects. Every time the source program says to read from or write to a volatile object, the compiled code must do so.
Even though volatile has a different meaning from const, you can still regard volatile as a promise. The presence of volatile in a declaration such as:
T volatile *pv;
is effectively a promise that the compiler won't try to optimize the accesses to or from any T object referenced via a value obtained directly or indirectly from pv. By analogy with const, this is not a promise that pv will point only to volatile T objects; pv can point to either volatile or nonvolatile T objects. It's just that pv promises to treat any T objects it might point to as if they all were volatile.
Once you understand volatile as a promise, it's no surprise that the conversion rules regarding "pointer to volatile" are identical to those regarding "pointer to const." For example, given:
T volatile *pv; ... void g(T *q);
you can't call g(pv). The declaration of pv promises that the program will treat whatever pv points to as if it were volatile. However, the declaration for g's parameter q makes no such promise. Therefore, the compiler can't entrust q with a copy of pv, because q might violate the promise.
On the other hand, given:
T *p; ... void h(T volatile *qv);
then calling f(p) compiles without complaint. In this case, p's declaration makes no promises, so calling h(p) can't violate anything. However, the declaration for h's parameter qv makes a promise that the compiler won't optimize the accesses to any T objects accessed via pv. The code generated for h's function body might be less than optimal, but it won't mistreat any T objects.
Who's making the promise?
If const and volatile are indeed promises, who's really making those promises?
Const is a promise that you make to the compiler. When you declare p as a "pointer to const T," you're promising the compiler that you (or your program, if you prefer) will treat *p as a nonmodifiable object. Moreover, you're enlisting the compiler's aid in helping you keep that promise, something it's more than happy to do.
On the other hand, volatile is a promise that you elicit from the compiler. When you declare p as a "pointer to volatile T," you're asking the compiler to promise you that it won't optimize any accesses to *p. In response, the compiler demands that you help it keep that promise by obeying certain conversion rules.
Many thanks to Joel Saks for very insightful help with this article.
Dan Saks is president of Saks & Associates, a C/C++ training and consulting company. For more information about Dan Saks, visit his website at www.dansaks.com. Dan also welcomes your feedback: e-mail him at firstname.lastname@example.org.
1. Saks, Dan. "Place volatile accurately," Embedded Systems Design, November 2005, p. 11.
2. Saks, Dan. "C++ Theory and Practice: const as a Promise," The C/C++ Users Journal, November 1996, p. 81.
3. Saks, Dan. "What const really means," Embedded Systems Programming, August 1998, p. 11.
Thank you very much for making me more clear about "volatile" and "const". It's wonderful to regard the type qualifiers as "promises".
Very good article. Simple but very clear.
- Daniel CABRERA
Lead Software Engineer