Is the aliasing rule symmetric?

  • Thread starter Johannes Schaub (litb)
  • Start date
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.
 
B

Ben Bacarisse

Joshua Maurice said:
On Jan 24, 8:10 pm, Ben Bacarisse <[email protected]> wrote:

Yes. I've been getting various replies when I tweak the above program
just slightly.

#include <stdlib.h>
void foo(int* a, float* b)
{
*a = 1;
*b = 1;
}
int main()
{
void* p = malloc(sizeof(int) + sizeof(float));
foo((int*)p, (float*)p);
}

I asked if this had UB in comp.lang.c a while ago. I received various
replies, with little follow up discussion.

One reply was that a piece of memory may have at most one effective
type between calls to malloc and free.

That seems to me to be clearly false. Here is the wording:

"The effective type of an object for an access to its stored value is
the declared type of the object, if any[75]. If a value is stored into
an object having no declared type through an lvalue having a type that
is not a character type, then the type of the lvalue becomes the
effective type of the object for that access and for subsequent
accesses that do not modify the stored value. If a value is copied
into an object having no declared type using memcpy or memmove, or is
copied as an array of character type, then the effective type of the
modified object for that access and for subsequent accesses that do
not modify the value is the effective type of the object from which
the value is copied, if it has one. For all other accesses to an
object having no declared type, the effective type of the object is
simply the type of the lvalue used for the access."

Footnote 75 says: "Allocated objects have no declared type."

The object being stored into has no declared type. The *a = 1; makes
the effective type of the allocated space 'int' for that access and for
subsequent accesses that do not modify the object, but *b = 1; does
modify the object so, again, the effective types becomes that of the
lvalue expression used in the store: 'float'.
Another reply was that this is a DR in the C and C++ language specs,
known colloquially as the union DR.

There is no union so unless the DR covers more than just unions it won't
apply.
Another reply was that the above program has perfectly well defined
behavior, but the following has undefined behavior:
#include <stdlib.h>
int foo(int* a, float* b)
{
*a = 1;
*b = 1;
return *a;
}
int main()
{
void* p = malloc(sizeof(int) + sizeof(float));
foo((int*)p, (float*)p);
}

Yes, that's undefined. Sadly, I miss-read a similar example elsewhere
in this thread. After *b = 1; the effective type is float and the
access through an lvalue expression on type int is undefined.
Specifically, this example explains how the compiler might use
aliasing analysis for optimization purposes. A conforming compiler may
not simply assume that an int* and a float* do not alias. However, if
analysis shows that aliasing would result in UB (as it would in the
above program when "return *a;" reads a float object through an int
lvalue) then the compiler is free to do whatever it wants in the face
of the UB, including assume that they don't alias.

I think I like the third option best, but my personal preferences
don't dictate what compilers actually do.

No, nor mine, but that last explanation seems to me to be the correct
one.

An example that might distinguish between a compiler that assumes no
aliasing and one that knows the effective type rules would be this:

#include <stdlib.h>
#include <stdio.h>

int foo(int *a, float *b)
{
int x = *a;
*b = 1;
return x + *b;
}

int main(void)
{
void *p = malloc(sizeof(int) + sizeof(float));
*(int *)p = 1;
printf("%d\n", foo((int *)p, (float *)p));
printf("%f\n", *(float *)p);
return 0;
}

I think James posted a similar example elsewhere. In C this is
well-define and must print 2 and 1.000000 (or thereabouts). If a
compiler just assumes that 'a' and 'b' in foo can never point to the
same object, it might produce the wrong result (by, for example,
optimising 'x' away and using *a in the return).

It is possible that the C committee intended that the rules would allow
a compiler to assume that 'a' and 'b' don't alias, but that is certainly
not how I read the rules as they stand.
 
J

Johannes Schaub (litb)

Ben said:
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 don't think so. In foo the order does matter if both x and y point to the
same allocated object. The caller of the function might want to access the
memory later on with a short lvalue. Reordering the assignments will cause
UB in the caller.
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".

I didn't consider this at all before. Interesting!

A store modifies the effective type of an object. The spec says "If a value
is stored into an object having no declared type through an lvalue having a
type that is not a character type, then the type of the lvalue becomes ...",
therefor if you store an int, and then store a float, the object's effective
type isn't int anymore but becomes a float. It can't be two. At least that
wouldn't make sense to me.
 
B

Ben Bacarisse

Johannes Schaub (litb) said:
I don't think so. In foo the order does matter if both x and y point to the
same allocated object. The caller of the function might want to access the
memory later on with a short lvalue. Reordering the assignments will cause
UB in the caller.

Good point -- I missed that. I'd word it rather differently though.
The UB (or lack of it) is a property of the text (the program's text)
and not a property that can be caused by what a compiler chooses to do.
I'd phrase it like this: the caller must see the value 2 when accessing
the allocated object via an lvalue expression of type short. If it uses
a char type, it must see (one of) the representations of a short int
with value 2. Any other result means that the implementation is
non-conforming.
I didn't consider this at all before. Interesting!


A store modifies the effective type of an object. The spec says "If a value
is stored into an object having no declared type through an lvalue having a
type that is not a character type, then the type of the lvalue becomes ...",
therefor if you store an int, and then store a float, the object's effective
type isn't int anymore but becomes a float. It can't be two. At least that
wouldn't make sense to me.

Yes, it can't be two at the same time. I hope I did not imply it might
be (though I completely missed the point of the function bar so who know
what impression that gave). Code that only does stores into an
allocated object won't fall foul of the access rules. I think we are in
agreement about that.
 
J

James Kanze

Indeed. It's all very related to the union DR. So, the C standard
committee never intended for the following program to have defined
behavior? Interesting.
void foo(int* x, float* y)
{ *x = 1;
*y = 1;
}
int main()
{ union { int x; float y; } u;
foo(&u.x, &u.y);
return u.y;
}

That's what I vaguely remember. But I don't remember who was
saying this (someone authorized to speak for the committe, or
not?), nor the exact context (other than it was in relationship
with the gcc bug). The best might be to take the discussion to
comp.std.c (although I don't read that).

And once we find out what was intended for C, we then have to
address the issue in C++; I'm still supposing that C++ wants
full C compatibility in this regard. (Or is that wishful
thinking on my part.)
 
K

Keith Thompson

Do you mean the lifetime of a POD *object*?
That's a good question: do PODs have lifetime? I'd argue yes,
but it's not the lifetime defined in §3.8. Accessing an
uninitialized POD is undefined behavior, and if you can't access
an object, how can you say it exists?

At least in C, "access" includes both reading and modifying.
You can certainly modify an uninitialized POD object, something
you couldn't do if it didn't exist.

If you mean the lifetime of the object, why wouldn't it be the lifetime
defined in 3.8? If you mean the lifetime of the class, I'm not sure
what that would mean.

[...]
 
J

Joshua Maurice

Do you mean the lifetime of a POD *object*?

Yes. Of course. Sorry.
At least in C, "access" includes both reading and modifying.
You can certainly modify an uninitialized POD object, something
you couldn't do if it didn't exist.

If you mean the lifetime of the object, why wouldn't it be the lifetime
defined in 3.8?  If you mean the lifetime of the class, I'm not sure
what that would mean.

I think James was referring to how the lifetime rules for POD objects
are broken in C++. It says the lifetime of a POD object begins as soon
as memory of sufficient size and alignment is allocated, which is
absurd, because that would imply quite a large number of complete
objects of completely different types coexisting in the same piece of
memory for every piece of memory.

I'm not sure of the rules for C. Again, can you answer my questions
else-thread in the example which attempts to distinguish between the
following?
typedef struct T1 { int x; int y; } T1;
typedef struct T2 { int x; int y; } T2;

I think I'll take James's advice and take this to comp.std.c.
Hopefully they're more talkative than comp.std.c++.
 
J

Johannes Schaub (litb)

Joshua said:
Yes. Of course. Sorry.


I think James was referring to how the lifetime rules for POD objects
are broken in C++. It says the lifetime of a POD object begins as soon
as memory of sufficient size and alignment is allocated, which is
absurd, because that would imply quite a large number of complete
objects of completely different types coexisting in the same piece of
memory for every piece of memory.

I'm not sure of the rules for C. Again, can you answer my questions
else-thread in the example which attempts to distinguish between the
following?
typedef struct T1 { int x; int y; } T1;
typedef struct T2 { int x; int y; } T2;

I think I'll take James's advice and take this to comp.std.c.
Hopefully they're more talkative than comp.std.c++.

T1 and T2 are different types in C. I cannot find a rule that says that
these are compatible. So since they are different types and there is no rule
saying otherwise, the are not compatible. Thus they cannot alias each other.

All of this thread was crossposted to comp.std.c. I installed a followup-to
header that posted all this to comp.lang.c++, comp.lang.c and comp.std.c.
 
J

Joshua Maurice

T1 and T2 are different types in C. I cannot find a rule that says that
these are compatible. So since they are different types and there is no rule
saying otherwise, the are not compatible. Thus they cannot alias each other.

I can also repeat that verbatim. Can you answer the more specific
questions which I have? I can copy and paste them into a new reply, if
you want.
 
J

James Kuyper

I think James was referring to how the lifetime rules for POD objects
are broken in C++. It says the lifetime of a POD object begins as soon
as memory of sufficient size and alignment is allocated, which is
absurd, because that would imply quite a large number of complete
objects of completely different types coexisting in the same piece of
memory for every piece of memory.

I think it's clear from the context that this rule does not refer to
arbitrary pieces of memory, but only the particular piece of memory
allocated for that particular object, which has only that object's
specific type.
 
J

Joshua Maurice

I think it's clear from the context that this rule does not refer to
arbitrary pieces of memory, but only the particular piece of memory
allocated for that particular object, which has only that object's
specific type.

Let me ask the relevant questions again then. For the following
program, when does the lifetime of the T1 object begin? Why do we have
a T1 object instead of a T2 object? (See further questions inline in
the code.)

#include <assert.h>
#include <stddef.h>
#include <stdlib.h>

typedef struct T1 { int x; int y; } T1;
typedef struct T2 { int x; int y; } T2;

int main()
{
void* p = 0;
T1* t1 = 0;
T2* t2 = 0;

if (sizeof(T1) != sizeof(T2))
return 1;
if (offsetof(T1, x) != offsetof(T2, x))
return 1;
if (offsetof(T1, y) != offsetof(T2, y))
return 1;

p = malloc(sizeof(T1));
/* Has the lifetime of a T1 object started yet? What about T2? */

*(int*)(((char*)p) + offsetof(T1, x)) = 1;
/* Has the lifetime of a T1 object started yet? What about T2? */

*(int*)(((char*)p) + offsetof(T1, y)) = 2;
/* Has the lifetime of a T1 object started yet? What about T2? */

/* Can one even talk about the start of the lifetime of a T1
object at this point? Is there any reason to prefer talking about a T1
object over a T2 object? How is the above code importantly different
than casting the result of malloc to T1* (which is implicit in C),
using a memberof expression to get an lvalue for both of its members,
and writing to those members? Do we need to change the standards to
explicitly mention that the memberof expression is special with
regards to the object lifetime rules - specifically something like "A
memberof expression "X.Y" starts the lifetime of a new complete object
if there does not exist an object (complete or subobject) of the same
type as X at the memory referred to by X. The new complete object's
type is the same type as the left hand side of the memberof
expression."? I've already mused about such a possibility on comp.std.c
++, but such an idea just strikes me as exceptionally silly. However,
I think that's our only real way out at the moment. */

/* Where is the UB in the following code, for exactly what reason?
*/
t1 = (T1*)p;
printf("%d\n", t1->y);
t2 = (T2*)p;
printf("%d\n", t2->y);
}
 
B

Ben Bacarisse

Joshua Maurice said:
I can also repeat that verbatim. Can you answer the more specific
questions which I have? I can copy and paste them into a new reply, if
you want.

I am not Johannes Schaub, but I don't mind trying to answer your
specific questions. I think they raise interesting questions and all I
can do is give you my best guess.

| We need to solve a couple of basic problems. The most important and
| basic is: when does the lifetime of a POD class even begin? Consider:

I don't think that's what you want to ask. You seem to be asking when
an allocated object acquires an effective type. In the code below the
lifetime of the allocated object extends from the execution of the
malloc to the program's termination but that's not the important issue.

|
| #include <cstdlib>
| using namespace std;
|
| struct T1 { int x; int y; };
| struct T2 { int x; int y; };
|
| int main()
| {
| void* p = 0;
| T1 * t1 = 0;
| T2 * t2 = 0;
| int * x = 0;
|
| if (sizeof(T1) != sizeof(T2))
| return 1;
| if ( (char*)(& t1->y) - (char*) (& t1) != (char*)(& t2->y) -
| (char*) (& t2) )

You corrected this in a subsequent posting but I think it is better
expressed with offsetof:

if (offsetof(struct T1, y) != offsetof(struct T2, y))

| return 1;
|
| p = malloc(sizeof(T1));
| /* Do we have a T1 object here? Presumably no.

No, there is only an object with no declared type and no effective type.

| Otherwise we also
| have a T2 object here, and we definitely don't want to start talking
| about two distinct complete objects occupying the same storage at the
| same time. */

That does not follow -- having a T1 does not mean we'd have a T2.

memcpy(p, &(struct T1){0, 0}, sizeof (struct T1));

gives the allocated object the effective type "struct T1". In effect
there is now a struct T1 there and nothing else, though that is not a
particularly good way of putting it.

| t1 = (T1*) p;
| /* T1 object yet? Presumably the answer hasn't changed since the
| above comment. */

Agreed.

| x = & t1->x;
| /* T1 object yet? */

In C, your question is a shorthand for "is the effective type of the
allocated object struct T1 yet?". My answer is no I see no reason for
the effective type to have changed.

| *x = 1;
| /* Do we have a T1 object here? Maybe. I just see a write through
| an int lvalue. I see no writes nor reads through a T1 lvalue.

No, I'd say definitely not. We have an int there and that is all. As
you say there is no write through an lvalue expression of type struct
T1.

| I see
| nothing that favors T1 over T2, besides some sort of data dependency
| analysis through the member-of operator. However, there isn't even a
| hint of data dependency analysis in the standard with regards to
| object lifetime rules. */

Talk of object lifetime rules is a C++ issue. In the C version of the
code there is only on object whose lifetime matter here -- the allocated
object and it persists to the end of the program's execution.

| x = & t1->y;
| *x = 2;
| /* Do we have a T1 object here?

I'd say no. I can't see any reason to think otherwise. You reasoning
above (there having been write through an lvalue expression of type
struct T1) still applies.

| The answer must be yes, or we'll
| never have a T1 object.

I don't think so. There are unequivocal way to get a struct T1
object (translation: for the effective type of the allocated object to
be struct T1). One is the memcpy I showed. The other would be a direct
assignment like:

*t1 = (struct T1){0, 0};

But the question you raise is an interesting one. We've set two
sub-objects inside one allocated object (thereby setting the effective
type of those two sub-objects) but we've not set the effective type of
the whole object (at least that's my reading of the standard).

| However, again, I see nothing to favor having
| a T1 object over a T2 object besides data dependency analysis through
| the member-of operator. */

Yes, we have two ints and neither a struct T1 nor a struct T2.

| t2 = (T2*) p;
| return t2->y; /* UB? Why? Why is reading "t1->y" not UB, but
| reading "t2->y" is UB? In other words, why do we have a T1 object, but
| not a T2 object? */

I'd say we have neither.

I have to conclude that neither t1->y not t2->y is undefined. Both
access an object with effective type int though and lvalue expression of
type int.

I say "have to" because I am not sure that this is what was intended.
Having gone round this a few times now I can no longer see the purpose
of the wording relating to aggregates and unions in 6.5 p7. Maybe some
kind c.l.c soul will explain it to me! Or maybe it will be clear to me
in the morning.

| }
|
| Also, what if we used offsetof hackery to initialize both int members
| of the T1 object without using a member-of operator on a T1 lvalue?
 
J

Joshua Maurice

|  
|     p = malloc(sizeof(T1));
|     /* Do we have a T1 object here? Presumably no.

No, there is only an object with no declared type and no effective type.

|                                                     Otherwise we also
| have a T2 object here, and we definitely don't want to start talking
| about two distinct complete objects occupying the same storage at the
| same time. */

That does not follow -- having a T1 does not mean we'd have a T2.

  memcpy(p, &(struct T1){0, 0}, sizeof (struct T1));

gives the allocated object the effective type "struct T1".  In effect
there is now a struct T1 there and nothing else, though that is not a
particularly good way of putting it.

Of course. I merely meant to argue that the effective type of the
allocated object cannot be T1 just after the malloc. There is
absolutely nothing in the code thus far to favor T1 over T2.
|     x = & t1->y;
|     *x = 2;
|     /* Do we have a T1 object here?

I'd say no.  I can't see any reason to think otherwise.  You reasoning
above (there having been write through an lvalue expression of type
struct T1) still applies.

|                                      The answer must be yes, or we'll
| never have a T1 object.

I don't think so.  There are unequivocal way to get a struct T1
object (translation: for the effective type of the allocated object to
be struct T1).  One is the memcpy I showed.  The other would be a direct
assignment like:

  *t1 = (struct T1){0, 0};

But the question you raise is an interesting one.  We've set two
sub-objects inside one allocated object (thereby setting the effective
type of those two sub-objects) but we've not set the effective type of
the whole object (at least that's my reading of the standard).

So, let me ask this then:

#include <assert.h>
#include <stddef.h>
#include <stdlib.h>

typedef struct T1 { int x; int y; } T1;

int main()
{
void* p = malloc(sizeof(T1));
T1* t = (T1*)p;
t->x = 1;
t->y = 2;
return t->y;
}

I am correct in assuming that this is a very C idiomatic way of
initializing malloc-ed memory, right?

Is this significantly different than?

#include <assert.h>
#include <stddef.h>
#include <stdlib.h>

typedef struct T1 { int x; int y; } T1;

int main()
{
void* p = malloc(sizeof(T1));
* (int*) (((char*)p) + offsetof(T1, x)) = 1;
* (int*) (((char*)p) + offsetof(T1, y)) = 2;
return ((T1*)p)->y;
}

Is one good and the other not? If so, what's the important difference,
and most importantly what part of the standard, if any, can be read to
describe that difference?
 
J

James Kanze

Do you mean the lifetime of a POD *object*?
Yes.
At least in C, "access" includes both reading and modifying.
You can certainly modify an uninitialized POD object, something
you couldn't do if it didn't exist.

Accessing an object certainly includes both reading and writing.
Maybe I'm reading too much into it, but the C++ statement talks
about "accessing an object's value"---I'm interpreting this to
mean an lvalue to rvalue conversion.
If you mean the lifetime of the object, why wouldn't it be the lifetime
defined in 3.8? If you mean the lifetime of the class, I'm not sure
what that would mean.

Yes. I'm referring to 3.8 in the C++ standard.
 
J

James Kanze

The interesting question in C is are they still not compatible
types if you drop the tags after the struct? (In C++, they are,
because in C++, if a class or struct doesn't have a tag, and
is in a typedef, the first name given for the type in the
typedef acts effectively as if it were a tag.)
| We need to solve a couple of basic problems. The most important and
| basic is: when does the lifetime of a POD class even begin? Consider:
I don't think that's what you want to ask. You seem to be asking when
an allocated object acquires an effective type. In the code below the
lifetime of the allocated object extends from the execution of the
malloc to the program's termination but that's not the important issue.

He's coming from a C++ background, where an object's lifetime is
related to its type: an object without a type cannot be alive.
But I like the way you describe it.

[...]
You corrected this in a subsequent posting but I think it is better
expressed with offsetof:

if (offsetof(struct T1, y) != offsetof(struct T2, y))

| return 1;
|
| p = malloc(sizeof(T1));
| /* Do we have a T1 object here? Presumably no.
No, there is only an object with no declared type and no effective type.

Is that directly from the C standard? (I like the way it is
expressed. I think it's the clearest exposition of what I think
should be the specification.)

[...]
| nothing that favors T1 over T2, besides some sort of data dependency
| analysis through the member-of operator. However, there isn't even a
| hint of data dependency analysis in the standard with regards to
| object lifetime rules. */
Talk of object lifetime rules is a C++ issue. In the C version of the
code there is only on object whose lifetime matter here -- the allocated
object and it persists to the end of the program's execution.

Yes. When you have constructors and destructors, object
lifetime has a real signification. The problem in the C++
standard is that it tries to extend the term to objects without
constructors and destructors, in a way that probably doesn't
apply.

[...]
Yes, we have two ints and neither a struct T1 nor a struct T2.
| t2 = (T2*) p;
| return t2->y; /* UB? Why? Why is reading "t1->y" not UB, but
| reading "t2->y" is UB? In other words, why do we have a T1 object, but
| not a T2 object? */
I'd say we have neither.
I have to conclude that neither t1->y not t2->y is undefined. Both
access an object with effective type int though and lvalue expression of
type int.

The expression t1->y is, by definition, the same as (*t1).y.
And what is (*t1), if not an access to an object of T1 (that, we
agree, doesn't exist).
 
J

Joshua Maurice

The expression t1->y is, by definition, the same as (*t1).y.
And what is (*t1), if not an access to an object of T1 (that, we
agree, doesn't exist).

Let me try a devil's advocate position.

With regards to the current POSIX pthreads rules, and future C++0x
threading rules, is (*t1) a read or write for the purposes of
synchronization and race conditions? I would argue no.

Consider the initial conditions:
typedef struct T1 { int x; int y; } T1;
T1* t = malloc(sizeof(T1));
t->x = 1;
t->y = 2;

If we start off the following two threads simultaneously, we do not
have a race condition under any sane interpretation.
/* thread 1 */
printf("%d\n", t->x);

/* thread 2 */
t->y = 3;

As you noted, those threads are by definition equivalent to:
/* thread 1 */
printf("%d\n", (*t).x);

/* thread 2 */
(*t).y = 3;

Thus, what exactly does that "*t" mean? One of those expressions
clearly involves a read of the *t object (and/or one of its sub-
objects), and the other clearly involves a write of the *t object (and/
or one of its sub-objects).

I think under the traditional view, it's not a race condition because
we're only accessing two distinct sub-objects and never accessing the
complete object.

However, you called *t an access of the T1 object, the object with
effective type T1, whatever. I suppose we could consider all accesses
of the form *t for struct types to be reads and never writes, but I
think that's somewhat silly. I think you would be hard pressed to
defend a terminology which says that "t->y = 3" involves a read of the
*t object.

Alternatively, we could define "access" so that you can access an
object without reading or writing it - for whatever that would mean,
which I think is even sillier.

I don't think that the expression "*t", where t has a not primitive
type, is necessarily an access in the conventional sense.
Unfortunately, I don't like that conclusion.

Consider:
T1 * x;
T1 * y;
/* .. */
*x = *y;
In the last line above, *x and *y are both "involved" in accesses.
There is a read of the *y object, and there is a write to the *x
object.

Consider:
T1 * x;
T1 * y;
/* .. */
(*x).x = (*y).x;
In the last line above, there is still a read and a write, but now
it's a read of a member sub-object and a write to a member sub-object
- not the whole object. The obviousness of this comes from how this
interacts with threading (described above), and with volatile
(described below).

Thus, it seems that "*x" is sometimes a read of the whole object, and
sometimes it's just part of an expression to get an lvalue to one of
its sub-objects (not meant to be an exhaustive list of possible
uses).

I think the rules concerning volatile also show this clearly. Consider
the following:
#include <stdlib.h>
typedef struct T3 { volatile int x; volatile int y; } T3;
int main()
{
T3 * t = malloc(sizeof(T3));

(*t).x = 1; /* Ok. There was a write to the volatile x sub-object.
There was not a read of the volatile x sub-object, and there was
definitely not a write nor read of the volatile y sub-object. I also
argue that there wasn't a read nor write of the T3 object. */

*t = *t; /* Ok, here we have a read of both volatile sub-objects,
and a write to both volatile sub-objects. I would even call this a
read of the T3 object, and a write to the T3 object. */

*t; /* What does this do? I don't know. Is it required to read
both volatile members? Is it required to read neither? */
}

What if the T3 object itself was volatile qualified?
typedef struct T3 { volatile int x; volatile int y; } T3;
int main()
{
volatile T3 a;
volatile T3 * b = & a;
(*b).y = 2;
}
In the above program, is there a read or write to the x sub-object? I
would assume no. Moreover, is there any sort of guaranteed observable
behavior besides the write to the y sub-object? That is, does the top
level volatile in "volatile T3 a;" mean anything in this example?
AFAIK, the top level volatile qualifier here doesn't really do much
because all of the sub-objects are already volatile qualified.

I'm not really sure where I'm going with this though... It's too late
for me. I think I'm trying to get at is AFAIK there really is no such
thing as an access, read or write, of an object with non-primitive
type. All of the accesses are really accesses through primitive types.
This seems abundantly clear from the volatile rules and the rules for
race conditions with threading. Which of course begs the question
about how to make this sensible with the strict aliasing rules which
do talk about accesses of objects of non-primitive type, and the
general consensus which definitely wants to prohibit "accessing" a T1
object through a T2 object lvalue.

Ex: Assuming a compiler which gives these two types the same layout
and size:

typedef struct T1 { int x; int y; } T1;
typedef struct T2 { int x; int y; } T2;
int main()
{
T1 * x;
T2 * y;

x = new malloc(sizeof(T1));
x->x = 1;
x->y = 2;
y = (T2*) x;
return y->y; /* UB */
}

In short: the general consensus says that the above UB is UB because
there's an "access" of an object with effective type T1 through a T2
lvalue. I don't see any rules which explain why the effective type of
the object is T1 (explained in other posts), and I don't see any
meaningful rules which describe what it means to "access" an object
through a T2 lvalue (explained at length in this post).
 
W

Willem

James Kanze wrote:
) The expression t1->y is, by definition, the same as (*t1).y.
) And what is (*t1), if not an access to an object of T1 (that, we
) agree, doesn't exist).

(*t1) is only an access to an object if it is involved
in something else that implies access.

Consider the following: &(*t1)

( Or, when considering structs: &(t1->y) )


SaSW, Willem
--
Disclaimer: I am in no way responsible for any of the statements
made in the above text. For all I know I might be
drugged or something..
No I'm not paranoid. You all think I'm paranoid, don't you !
#EOT
 
B

Ben Bacarisse

James Kanze said:
The interesting question in C is are they still not compatible
types if you drop the tags after the struct?

No, I think not. Compatibility is defined by a set of rules and there
are only two might apply. One is "[t]wo types have compatible type if
their types are the same" but the description of a struct declaration
states that it introduces a new type. The second is a rule for
compatibility of structs declared in separate compilation units so it
does not apply here. The two types *would* be compatible if declared in
separate translation units (as you'd expect) but not otherwise.
(In C++, they are,
because in C++, if a class or struct doesn't have a tag, and
is in a typedef, the first name given for the type in the
typedef acts effectively as if it were a tag.)
| We need to solve a couple of basic problems. The most important and
| basic is: when does the lifetime of a POD class even begin? Consider:
I don't think that's what you want to ask. You seem to be asking when
an allocated object acquires an effective type. In the code below the
lifetime of the allocated object extends from the execution of the
malloc to the program's termination but that's not the important issue.

He's coming from a C++ background, where an object's lifetime is
related to its type: an object without a type cannot be alive.
But I like the way you describe it.

[...]
You corrected this in a subsequent posting but I think it is better
expressed with offsetof:

if (offsetof(struct T1, y) != offsetof(struct T2, y))

| return 1;
|
| p = malloc(sizeof(T1));
| /* Do we have a T1 object here? Presumably no.
No, there is only an object with no declared type and no effective type.

Is that directly from the C standard? (I like the way it is
expressed. I think it's the clearest exposition of what I think
should be the specification.)

Yes, pretty much. 6.5 p6 refers to a footnote that explains how some
objects have no declared type ("Allocated objects have no declared
type") and none of the rules that give an object an effective type (that
is a C standard term) apply yet so the object does not yet have one.
[...]
| nothing that favors T1 over T2, besides some sort of data dependency
| analysis through the member-of operator. However, there isn't even a
| hint of data dependency analysis in the standard with regards to
| object lifetime rules. */
Talk of object lifetime rules is a C++ issue. In the C version of the
code there is only on object whose lifetime matter here -- the allocated
object and it persists to the end of the program's execution.

Yes. When you have constructors and destructors, object
lifetime has a real signification. The problem in the C++
standard is that it tries to extend the term to objects without
constructors and destructors, in a way that probably doesn't
apply.

[...]
Yes, we have two ints and neither a struct T1 nor a struct T2.
| t2 = (T2*) p;
| return t2->y; /* UB? Why? Why is reading "t1->y" not UB, but
| reading "t2->y" is UB? In other words, why do we have a T1 object, but
| not a T2 object? */
I'd say we have neither.
I have to conclude that neither t1->y not t2->y is undefined. Both
access an object with effective type int though and lvalue expression of
type int.

The expression t1->y is, by definition, the same as (*t1).y.

Yes, but that's a C/C++ difference. It's not defined like that in C.
And what is (*t1), if not an access to an object of T1 (that, we
agree, doesn't exist).

In C, the text describing -> avoids all mention of an access to an object
of the struct type.

However, even if we write (*t1)->y the effect is *still* defined. If
the "whole object" still has no effective type, then the final sentence
of 6.5 p6 applies:

"For all other accesses to an object having no declared type, the
effective type of the object is simply the type of the lvalue used for
the access."

Thus *t1 (and *t2 for that matter) is being "accessed by an lvalue
expression" with "a type compatible with the effective type of the
object". These two quotes are from the wording from the next paragraph
(6.5 p7) that defines what accesses are permitted.

This effective type does not "persist". In the case of a store, the
text of 6.5 p6 reads

"the lvalue becomes the effective type of the object for that access
and for subsequent accesses that do not modify the stored value"

but no such wording applies to a read access. Thus we can even write

return (*t1).y + (*t2).y;

and still be in the land of defined behaviour.
 
K

Keith Thompson

Joshua Maurice said:
So, let me ask this then:

#include <assert.h>
#include <stddef.h>
#include <stdlib.h>

typedef struct T1 { int x; int y; } T1;

int main()
{
void* p = malloc(sizeof(T1));
T1* t = (T1*)p;
t->x = 1;
t->y = 2;
return t->y;
}

I am correct in assuming that this is a very C idiomatic way of
initializing malloc-ed memory, right?

There's usually no reason to store the result of malloc in a void*.
This is more idiomatic:

#include <stdlib.h>

typedef struct T1 { int x; int y; } T1;

int main(void)
{
T1 *p = malloc(sizeof *p);
p->x = 1;
p->y = 2;
return p->y;
}

Though personally I'd drop the typedef and refer to "struct T1"
directly.

(Note that the "void" keyword makes the declaration of main a
prototype; it's debatable whether it's necessary, but IMHO it's
at least better style.)

This "sizeof" trick for malloc avoids errors when the type of p changes;
you don't have to mention the type twice. This:

T1 *p = malloc(sizeof(T1));

may be more common out in the real world (outside this newsgroup), but I
prefer the "sizeof *p" trick myself.
Is this significantly different than?

#include <assert.h>
#include <stddef.h>
#include <stdlib.h>

typedef struct T1 { int x; int y; } T1;

int main()
{
void* p = malloc(sizeof(T1));
* (int*) (((char*)p) + offsetof(T1, x)) = 1;
* (int*) (((char*)p) + offsetof(T1, y)) = 2;
return ((T1*)p)->y;
}

Is one good and the other not? If so, what's the important difference,
and most importantly what part of the standard, if any, can be read to
describe that difference?

As a matter of style, it's a much more verbose way of saying essentially
the same thing.
 
J

Joshua Maurice

As a matter of style, it's a much more verbose way of saying essentially
the same thing.

So let me ask again, to you and anyone else. Is there any difference
between the two programs:

#include <stddef.h>
#include <stdlib.h>
typedef struct T1 { int x; int y; } T1;
typedef struct T2 { int x; int y; } T2;
int main(void)
{ T1 *p = malloc(sizeof *p);
p->x = 1;
p->y = 2;
return p->y;
}

and

#include <stddef.h>
#include <stdlib.h>
typedef struct T1 { int x; int y; } T1;
typedef struct T2 { int x; int y; } T2;
int main()
{
void* p = malloc(sizeof(T1));
* (int*) (((char*)p) + offsetof(T1, x)) = 1;
* (int*) (((char*)p) + offsetof(T1, y)) = 2;
return ((T1*)p)->y;
}

Specifically, I presume that everyone agrees C and C++ needs to
support the first program with no UB. The interesting questions I have
concern the second. Does the "return ((T1*)p)->y;" result in UB? Why?
What's the important different between these two programs, and
specifically the parts of the standards which explain the important
differences.

Also, if the second program has no UB, can we instead return "return
((T2*)p)->y;" for implementations which we've tested that T1 and T2
have equivalent layout? That is, it might not be a portable program,
but for those systems which there is no difference in layout, would
the access through T2 have UB? Why?
 

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

Forum statistics

Threads
473,769
Messages
2,569,579
Members
45,053
Latest member
BrodieSola

Latest Threads

Top