Re: Is the aliasing rule symmetric?
On Jan 26, 1:49 am, James Kanze <james.ka...@gmail.com> 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).
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).