Johannes said:
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;
}