No way to add volatile access?

  • Thread starter Johannes Schaub (litb)
  • Start date
B

Balog Pal

Formally, any use of the original reference is undefined
behavior, as far as the standard is concerned. As far as the
language is concerned, there is no object (known to the
language) at the address 0x318F.

Let's change the example somewhat:

int orig_port;
int volatile *p_port = (int volatile*)&orig_port;

Now how to interpret the standard on using *p_port?

and if we instead use

int volatile *p_port = new (&orig_port) volatile int;

? would the two pointers have different behavior? Would be pretty weird IMO.
From a QoI point of view, I would expect an implementation to
treat port as if it referred to a volatile object. Regardless
of how it was initialized.

Yeah, me too. And if the standard indeed leaves that part of behavior not
defined, it would be a Q issue against the standard.
In this case, I think, intentionally. The intent of the
standard with regards to volatile is largely to provide a hook
for implementations to define additional behavior not defined in
the standard.

AFAIK the semantic of volatile is said implementation-defined anyway. So
keeping the users in limbo wrt what actually is use of volatile is bad, and
buys nothing but pain. :( Until this thread it never occoured to me that
volatile data could exclude using a volatile lvalue, even less that an
implementation like Intel referred upstream could go out defending such an
interpretation.

As I recall the implementations I used, many defined MSR-s through (volatile
*) 0x.... notation, and in the DOX only stated that int<->pointer casting
is done via identity transformation. All the generated code certainly did
obey the "intended" volatile semantics, and using any freedom suggested here
to exist would put much life and property in danger.
 
J

Johannes Schaub (litb)

Balog said:
Let's change the example somewhat:

int orig_port;
int volatile *p_port = (int volatile*)&orig_port;

Now how to interpret the standard on using *p_port?

and if we instead use

int volatile *p_port = new (&orig_port) volatile int;

? would the two pointers have different behavior? Would be pretty weird
IMO.

This all comes down to the weird lifetime starting rules, i think. They
state if you have memory correctly aligned and sized for an object of type
T, lifetime of that object starts. I don't understand those rules, though,
because it means for a memory block of 1 byte, we have infinitely many
objects, each of a different type of size 1 byte that fits there.

The intent is, i believe, that once you write to memory using a type without
a non-trivial constructor, the type of the lvalue used becomes the type of
the object:

int *p = malloc(sizeof (int));
*p = 10; // the allocated object now has type 'int'.

It only seems logical to have this be true for "cv T" too, because that's
just as well a valid object type. But it is certainly *not* the intent of
the committee that once we use a const lvalue to modify some memory
location, the object located there becomes const.

After all, I believe that the mechanisms of object creation and lifetime
starting for objects that are not created by definitions, new-expressions or
temporaries (like in the above malloc example) are largely mysterious. Maybe
all my rant is just nonsense, but I've never met anyone who could explain
this mystery.

But independently of that, it's a fact that in your second example where you
use a placement new, you create a new object at that place by "reusing" the
storage (which the Standard says ends the lifetime of an object previously
located there). So after your placement-new, the Standard is clear that you,
when you use "*p_port", have a volatile object referenced.

About this very difference, there was a long GCC bug-report here:
http://gcc.gnu.org/bugzilla/show_bug.cgi?id=29286 . I find it quite amusing.
 
J

Johannes Schaub (litb)

Johannes said:
Balog said:
Let's change the example somewhat:

int orig_port;
int volatile *p_port = (int volatile*)&orig_port;

Now how to interpret the standard on using *p_port?

and if we instead use

int volatile *p_port = new (&orig_port) volatile int;

? would the two pointers have different behavior? Would be pretty weird
IMO.

snipped [...]
It only seems logical to have this be true for "cv T" too, because that's
just as well a valid object type. But it is certainly *not* the intent of
the committee that once we use a const lvalue to modify some memory
location, the object located there becomes const.

Um, wait. I wasn't thinking here. We can't use a const lvalue to modify
something, of course. So this isn't an issue. Then the question remains only
for volatile. Does the first write to a non-volatile object thru a volatile
lvalue change that object to be a volatile object? I think that issue is
similar to the issue of the "int *p = malloc" example.
 
J

Joshua Maurice

Johannes said:
Balog Pal wrote:
snipped [...]
It only seems logical to have this be true for "cv T" too, because that's
just as well a valid object type. But it is certainly *not* the intent of
the committee that once we use a const lvalue to modify some memory
location, the object located there becomes const.

Um, wait. I wasn't thinking here. We can't use a const lvalue to modify
something, of course. So this isn't an issue. Then the question remains only
for volatile. Does the first write to a non-volatile object thru a volatile
lvalue change that object to be a volatile object? I think that issue is
similar to the issue of the "int *p = malloc" example.

Technically, it says that the lifetime of an object with a trivial
constructor begins once proper storage with the proper alignment and
size has been allocated. If the class has a non-trivial constructor,
then its lifetime begins only after storage with proper alignment and
size has been allocated \and\ its constructor call completes. 3.8
Object Lifetime / 1

I agree that the 3.8 Object Lifetime rules are somewhat nonsensical
when you combine them with the strict aliasing rules of 3.10 Lvalues
and rvalues / 15. I read the 3.8 lifetime rules as you do, that a POD
object's lifetime begins once proper storage has been allocated, but
that means that a single piece of allocated storage has an infinite
amount of "objects" existing in it, one for every possible POD type of
that size or smaller. And you can't access any of those objects
arbitrarily because of the strict aliasing rules.

I have a certain idiom when working with such things, which is rarely.
This is what I take away as the intended reading, and probably what
most compilers will accept. Let's review some relevant portions of the
standard:

3.8 Object Lifetime / 4
The rules of 3.8 Object Lifetime / 4 make it explicitly clear that you
can construct a new object in place of an already existing object
without first freeing the original object, even if the original object
has a non-trivial destructor. (However, the program has undefined
behavior if anyone depend on that destructor call, whatever that
means. So, if you don't want memory leaks or other kind of resource
leaks, and if the original object has a non-trivial destructor, then
you should make sure to get it called before you reuse the storage for
another object, and before releasing that storage.)

3.7.3.1 Allocation functions / 2
Memory allocation functions, such as the namespace scope function
"operator new", allocate raw storage appropriately aligned for any
complete object of size equal to or less than the allocated size.

5.3.4 New / 10
When allocating a char or unsigned char array with a "new-expression",
aka with the operator named "new", it returns a piece of storage also
appropriately aligned. As char and char array have trivial
destructors, you can (re)use the storage exactly like the raw storage
returned from an allocation function like the namespace scope function
"operator new". (A note mentions that this is to support this very
idiom.)

3.10 Lvalues and rvalues / 15
These are the strict aliasing rules. A note mentions that the purpose
of these restrictions is to allow the compiler to know which pointers
can alias. As a matter of fact for certain implementations, such as
gcc, I know that they can and will assume that two sufficiently
differently typed pointers will not alias.

So, this is what I do to be sane, safe, and portable. It might be
overkill, but the rules of 3.8 Object Lifetime / 5 scare me, so I take
the conservative approach. Besides, this approach is not less powerful
or slower.

[3.8 Object Lifetime / 1] says that an object's lifetime with a
trivial constructor begins when storage of the proper size and
alignment has been allocated. I basically ignore that. Instead, I
access a heap object as type T only in the interval:
1- After a "new-expression" returns a pointer to type T which points
to that location, either the regular operator named "new" or placement
new.
2- Before that storage is released, "reused" by any other placement
new on that storage, or the object's lifetime ends from a non-trivial
destructor call.
(And I sometimes access a POD object through a char or unsigned char
pointer, though that's a whole other discussion, but only during the
time interval just outlined.)

I figure that this should keep me safe from the strict aliasing rules,
and it is a subset of the lifetime rules specified in the standard, so
that's the best I can do.

Ex:

#include <string>
#include <new>
int main()
{
char * a = new char[sizeof(int) + sizeof(std::string)];
/* Let's ignore exception safety for the moment. */

/* It's always fine to do the cast. It's just (potentially) bad to
"use" the pointer to access the object. */
int * b = reinterpret_cast<int*>(a);

/* I would not do this. By some reading of the standard, an int
object exists in this storage, but it seems to defeat the purpose of
the strict aliasing rules to think that way. */
*b = 1;

b = new(a) int;

/* This I would do. I use the result of a "new expression" without
any explicit casting. */
*b = 1;

std::string * c = new (a) std::string;

// bad
*b = 1;

// also fine.
*c = "foo";

// also bad
*b = 1;

/* Call nontrivial destructor before releasing or reusing the
storage. */
c -> std::string::~string();

// also bad
*b = 1;

b = new(a) int;

// ok
*b = 1;

delete[] a;
}
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,799
Messages
2,569,652
Members
45,385
Latest member
ZapGuardianReviews
Top