B
Ben Bacarisse
James Kanze said:Hopefully, C++ says the same thing. This is one point where
I don't think the languages should differ.
Gcc treates it as undefined behavior if there is aliasing, and
may reorder the assignments. (IMHO, this is an error, but from
what I understand, it is an important optimization in certan
cases.)
How annoying. I was trying to clarify things and I went and miss-read
the code -- I read the *x return as *y. bar *is* undefined but I was
correct about foo: it is not undefined (in C).
Gcc may re-order the statements because in foo the order does not matter
and in bar the return causes UB.
I'm not sure I understand. Supposing that x and y point to the
same address, which was obtained by malloc. If the memory is
uninitialized, there is undefined behavior.
What I should have said is that the code is not always UB. Yes it can
be UB (and it is in almost every imaginable case where you might see
this code) but it does not have to be.
If the memory was
initialized as an int, then accessing it as a short is undefined
behavior, and if it was initialized as a short, accessing it as
an int has undefined behavior. And there's no way for its
"effective type" to be both short and int; it's one or the
other (or none of the above), but it can't be both.
Yes, the effective type can't be two types at once but it can be none.
If the object is zeroed with memset before the call (or it was allocated
using calloc) it still has no effective type (at least this is my
reading of the situation) and so the "effective type of the object is
simply the type of the lvalue used for the access".
In C (and C++), when the memory is allocated, it is
uninitialized. I don't know what type, if any, it is assumed to
have, but regardless of the type, you simply cannot access
uninitialized memory (except through an unsigned char*). And
once you initialize it, you've fixed the type (until the next
"initialization", at least).
I don't think memset sets the effective type. A clearer example might
be to use callocd space rather than mallocd space, but in either case if
all you do is stores (like the first foo function above) then C defines
the result. C++'s rules are (naturally) much more complicated and I
don't pretend to understand them yet, but from other posts I gather that
C++ also permits such code -- the write though the pointer effectively
starting a new object lifetime.
What is the effect of memset(..., 0, ...) on such an object in C++? In
C++ what accesses are permitted after zeroing with memset? I think that
in C the effective type is erased (the object will have no effective
type) and the access rules are written to handle that case explicitly.
That wording is not there in C++ and I can't yet work out the
consequences of that difference.
And the C++ standard clearly says:
If a program attempts to access the stored value of an
object through an lvalue of other than one of the following
types the behavior is undefined:
[...]
and short for int or vice versa isn't in the list. And I'm
certain that the intent in C is the same: C definitly allows
trapping representations for integer values, and reading part of
an int as a short could conceivably result in a trapping
representation for a short. (Think of a one's complement
machine which traps on -0.)
The problem becomes more interesting if we replace short with
unsigned char. In that case, my version is legal and defined
behavior: accessing a stored value through an lvalue of char or
unsgiend char type is in the list after the cited paragraph.
(IIRC, in C, this exception only applies to unsigned char; for
some reason, C++ added plain char to the list.)In C, the wording is "a character type" which covers char and both
signed and unsigned char. As you say, it is odd (at last at first
glance -- I am not a C++ expert) that C++ added char but not signed
char to the list.
It's especially odd that signed char is allowed, since copying
a signed char cannot necessarily be made to preserve the raw
bits or avoid trapping.
I don't think it's odd, but that is ultimately a matter of opinion. I
think it's simpler to include all char types or, alternatively, to
permit only unsigned char which neither language has done (for entirely
reasonable historical reasons).
(Again, a machine with 1's complement
which either converts all 0's the positive representation when
it sees them, or traps. On such machines, for plain char to
work, it would have to be unsigned---in fact, on the two
machines I know which don't use 2's complement, plain char is
unsigned.)
Just an idea: for purposes of demonstration, it might be better
to use int and float, rather than int and short, because reading
an int as a float can trap on most common machines; we don't
have to introduce such exotics as 1's complement to cause
issues.
I think traps complicate rather than clarify the argument. It would be
better to restrict the discussion to the access -- is the access itself
permitted or undefined -- rather than add the complication of whether
the result is a trap representation. After all, many accesses that are
unquestionably well-defined can yield a trap representation and many
unquestionably undefined accesses don't involve traps in any way.
To understand C and C++'s access rules I'd suggest we pick examples
where traps don't enter into the picture -- say by using integer types
and declaring that there are no trap representations.