I’ve seen constructs like the following to write to memory mapped I/O.
*((volatile unsigned int *)0xDEADBEEF) = 0x00;
But is this guaranteed to be a volatile access?1
I started to think about this after I’ve noticed following change in the C standard regarding cast operators:
The C11 spec says following:
N1570, § 6.5.4, ¶ 5:
Preceding an expression by a parenthesized type name converts the value of the expression to the named type. This construction is called a cast.104) […]
N1570, footnote 104):
- A cast does not yield an lvalue. Thus, a cast to a qualified type has the same effect as a cast to the unqualified version of the type.
In C17 this has changed to:
N2310, § 6.5.4, ¶ 5:
Preceding an expression by a parenthesized type name converts the value of the expression to the unqualified version of the named type. This construction is called a cast.108) […]
N2310, footnote 108):
- A cast does not yield an lvalue.
Both versions also have:
N2310, § 6.5.3.2, ¶ 4:
The unary * operator denotes indirection. […]; if it points to an object, the result is an lvalue designating the object. If the operand has type “pointer to type“, the result has type “type“. […]
So for C17 it should be clear: the above shown code should be the same as *((unsigned int *)0xDEADBEEF) = 0x00;
, i.e., the volatile qualifier is dropped and thus this wouldn’t be a volatile access, although we have the volatile
in the cast expression.
For C11 I am not sure: if we take the footnote as obligatory, then it shouldn’t be a volatile access either, because “a cast to a qualified type has the same effect as a cast to the unqualified version of the type”; but without that footnote and just the text in (§ 6.5.4, ¶ 5) in mind, I would say it should be a volatile access – or are there some other paragraphs in the spec which make it clear that this should not be a volatile access?
For the sake of completeness: following should be guaranteed to be a volatile access:
volatile unsigned int *mem = (unsigned int *)0xDEADBEEF;
*mem = 0x00;
1Assuming that a write to an object of volatile-qualified type is defined as an access to that object; see following:
N1570, § 6.7.3, ¶ 8:
[…] What constitutes an access to an object that has volatile-qualified type is implementation defined.
0
The two versions of the standard are essentially saying the same thing in two different ways, i.e. that a cast effectively results in a unqualified type.
What you seem to be confused by here is that you’re not actually casting to a qualified type. You’re casting to a pointer (which is unqualified in this case) to a qualified type. The pointer itself is not qualified, but what it points to is.
So this:
*((volatile unsigned int *)0xDEADBEEF) = 0x00;
Is considered volatile access, as the constant 0xDEADBEEF
is converted to a pointer (which is not volatile) to a volatile unsigned int
, and that pointer is subsequently dereferenced to access the volatile object.
The change in 6.5.4 5 between C 2011 and C 2018:
-
was intended be a correction and clarification, not a change in what is specified (per C 2018 Forward 8), and
-
is not relevant to your example because
volatile unsigned int *
is not a qualified type.
volatile unsigned int *
is a pointer type that has no qualifiers—an object of this type is a pointer that is not volatile. It points to a type that is qualified. The volatile
applies to the pointed-to type, not the pointer type, and it is not dropped by the cast. The result of the cast is an volatile unsigned int *
, including the volatile
.
… that footnote… [“A cast does not yield an lvalue.”]
This means the pointer resulting from the cast is not an lvalue. You could not write ((volatile unsigned int *) 0xDEADBEEF) = p;
to change the pointer resulting from the cast, just as you could not write 3+4 = 8;
to change the result of 3+4
.
When you apply the *
operator, that produces an lvalue from the pointer; *((volatile unsigned int *) 0xDEADBEEF)
is an lvalue, and it has volatile-qualified type.
On the topic of volatile accesses, C 2024 did make some changes. C 2018 mentions in various places accessing a “volatile object.” C 2024 instead defines a volatile access, in 5.2.2.4:
An access to an object through the use of an lvalue of volatile-qualified type is a volatile access.
and uses this term such as including volatile accesses in observable behavior:
Volatile accesses to objects are evaluated strictly according to the rules of the abstract machine.
I regard this as a clarification of language, not a change of intent. With C 2018, one might think that a volatile object is only one defined with volatile and have some question about * (volatile int *) &x = 3;
if x
was not defined with volatile
. If it was not defined with volatile
, does that assignment have to be evaluated as if it were?
C 2024 makes this clear; since a volatile-qualified type is used to access x
, it is a volatile access, regardless of how x
was defined.