The statement:
volatile unsigned char * volatile p = (volatile unsigned char * volatile)v;
Generates a warning C4197 in MSVC v14.1:
To make the code volatile unsigned char * volatile p = (volatile unsigned char * volatile) v;
compile in C or in C++ without warnings and while retaining the author’s intent, remove the second volatile
in the cast:
volatile unsigned char * volatile p = (volatile unsigned char *) v;
The cast is unnecessary in C, but the question asks that the code be compilable without warning in MSVC, which compiles as C++, not C, so the cast is needed. In C alone, if the statement could be (assuming v
is void *
or is compatible with the type of p
):
volatile unsigned char * volatile p = v;
The original source contains this code:
volatile unsigned char *volatile pnt_ =
(volatile unsigned char *volatile) pnt;
size_t i = (size_t) 0U;
while (i < len) {
pnt_[i++] = 0U;
The apparent desire of this code is to ensure that memory is cleared for security purposes. Normally, if C code assigns zero to some object x
and never reads x
before a subsequent assignment or program termination, the compiler will, when optimizing, remove the assignment of zero. The author does not want this optimization to occur; they apparently intend to ensure that memory is actually cleared. Clearing memory can reduce opportunities for an attacker to read the memory (through side channels, by exploiting bugs, by gaining physical possession of the computer, or other means).
Suppose we have some buffer x
that is an array of unsigned char
. If x
were defined with volatile
, it is a volatile object, and the compiler always implements writes to it; it never removes them during optimization.
On the other hand, if x
is not defined with volatile, but we put its address in a pointer p
that has a type pointer to volatile unsigned char
, what happens when we write *p = 0
? As R.. points out, if the compiler can see that p
points into x
, it knows that the object being modified is not volatile, and therefore the compiler is not required to actually write to memory if it can otherwise optimize away the assignment. This is because the C standard defines volatile
in terms of accessing volatile objects, not merely accessing memory through a pointer that has a type of “pointer to volatile something.”
To ensure the compiler actually writes to x
, the author of this code declares p
to be volatile. What this means is that, in *p = 0
, the compiler cannot know that p
points into x
. The compiler is required to load the value of p
from whatever memory it has assigned for p
; it must assume p
may have changed from the value that pointed into x
.
Further, when p
is declared volatile unsigned char *volatile p
, the compiler must assume that the place pointed to by p
is volatile. (Technically, when it loads the value of p
, it could examine it, discover it is in fact pointing into x
or some other memory known not to be volatile, and then treat it as non-volatile. But this would be an extraordinary effort by the compiler, and we can assume it does not happen.)
Therefore, if the code were:
volatile unsigned char *pnt_ = pnt;
size_t i = (size_t) 0U;
while (i < len) {
pnt_[i++] = 0U;
then, whenever the compiler can see that pnt
in fact points to non-volatile memory and that memory is not read before it is later written, the compiler may remove this code during optimization. However, if the code is:
volatile unsigned char *volatile pnt_ = pnt;
size_t i = (size_t) 0U;
while (i < len) {
pnt_[i++] = 0U;
then, in each iteration of the loop, the compiler must:
pnt_
from the memory allocated for it.Thus, the purpose of the second volatile
is to hide from the compiler the fact that the pointer points to non-volatile memory.
Although this accomplishes the author’s goal, it has the undesired effects of forcing the compiler to reload the pointer in each iteration of the loop and preventing the compiler from optimizing the loop by writing to the destination several bytes at a time.
Consider the definition:
volatile unsigned char * volatile p = (volatile unsigned char * volatile) v;
We have seen above that the definition of p
as volatile unsigned char * volatile
is needed to accomplish the author’s goal, although it is an unfortunate workaround to shortcomings in C. However, what about the cast, (volatile unsigned char * volatile)
.
First, the cast is unnecessary, as the value of v
will be automatically converted to the type of p
. To avoid the warning in MSVC, the cast can simply be removed, leaving the definition as volatile unsigned char * volatile p = v;
.
Given that the cast is there, the question asks whether the second volatile
has any meaning. The C standard explicitly says “The properties associated with qualified types are meaningful only for expressions that are lvalues.” (C 2011 [N1570] 6.7.3 4.)
volatile
means something unknown to the compiler can change the value of an object. For example, if there is a volatile int a
in the program, that means the object identified by a
can be changed by some means not known to the compiler. It could be changed by some special hardware on the computer, by a debugger, by the operating system, or by other means.
volatile
modifies an object. An object is a region of data storage in memory that can represent values.
In expressions, we have values. For example, some int
values are 3, 5, or −1. Values cannot be volatile. They are not storage in memory; they are abstract mathematical values. The number 3 can never change; it is always 3.
The cast (volatile unsigned char * volatile)
says to cast something to be a volatile pointer to a volatile unsigned char. It is fine to point to a volatile unsigned char
—a pointer points to something in memory. But what does it mean to be a volatile pointer? A pointer is just a value; it is an address. Values do not have memory, they are not objects, so they cannot be volatile. So the second volatile
in the cast (volatile unsigned char * volatile)
has no effect in standard C. It is conforming C code, but the qualifier has no effect.