|
|||
|
On Jan 25, 11:17 am, Joshua Maurice <joshuamaur...@gmail.com> wrote:
> On Jan 25, 2:22 am, James Kanze <james.ka...@gmail.com> wrote: > > If you're actually trying to write an allocator, you also have > > to take into account what actual compilers do, and not just the > > standard. I seem to recall something along the following lines: > > float f(float const* in, bool* out) > > { > > float result = *in; > > *out = true; > > return result; > > } > > failing with g++ when called with: > > union U { float f; bool b; }; > > U u; > > u.f = 3.14159; > > float g = f(&u.f, &u.b); > > According to both C and C++, the union guaranteed that this > > should work, But g++ rearranged the read and the write in f. > > (I also seem to recall---albeit vaguely---the C committee saying > > that it wasn't the intent to make this work; that they only > > meant for it to be guaranteed if e.g. f was passed a pointer to > > the union. But it's all very vague---I didn't have time to > > follow up at the time.) > > At any rate, most of this discussion seems to turn around the > > same issues, without the union. > 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.) -- James Kanze |
|
|
||||
|
||||
|
|
|
|||
|
James Kanze <james.kanze@gmail.com> writes:
> On Jan 24, 11:44 pm, Joshua Maurice <joshuamaur...@gmail.com> wrote: [...] >> 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? 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. [...] -- Keith Thompson (The_Other_Keith) kst-u@mib.org <http://www.ghoti.net/~kst> Nokia "We must do something. This is something. Therefore, we must do this." -- Antony Jay and Jonathan Lynn, "Yes Minister" |
|
|||
|
On Jan 25, 11:59*am, Keith Thompson <ks...@mib.org> wrote:
> James Kanze <james.ka...@gmail.com> writes: > > On Jan 24, 11:44 pm, Joshua Maurice <joshuamaur...@gmail.com> wrote: > [...] > >> 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? > > Do you mean the lifetime of a POD *object*? Yes. Of course. Sorry. > > 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. 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++. |
|
|||
|
Joshua Maurice wrote:
> On Jan 25, 11:59 am, Keith Thompson <ks...@mib.org> wrote: >> James Kanze <james.ka...@gmail.com> writes: >> > On Jan 24, 11:44 pm, Joshua Maurice <joshuamaur...@gmail.com> wrote: >> [...] >> >> 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? >> >> Do you mean the lifetime of a POD *object*? > > Yes. Of course. Sorry. > >> > 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. > > 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. |
|
|||
|
On Jan 25, 1:35*pm, "Johannes Schaub (litb)" <schaub-johan...@web.de>
wrote: > Joshua Maurice wrote: > > 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. 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. |
|
|||
|
On 01/25/2011 04:05 PM, Joshua Maurice wrote:
> On Jan 25, 11:59�am, Keith Thompson<ks...@mib.org> wrote: .... >> 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 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. -- James Kuyper |
|
|||
|
On Jan 25, 5:28*pm, James Kuyper <jameskuy...@verizon.net> wrote:
> On 01/25/2011 04:05 PM, Joshua Maurice wrote: > > > On Jan 25, 11:59 am, Keith Thompson<ks...@mib.org> *wrote: > ... > >> 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 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); } |
|
|||
|
Joshua Maurice <joshuamaurice@gmail.com> writes:
> On Jan 25, 1:35Â*pm, "Johannes Schaub (litb)" <schaub-johan...@web.de> > wrote: >> Joshua Maurice wrote: >> > 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. > > 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. Joshua Maurice <joshuamaurice@gmail.com> writes: | 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? -- Ben. |
|
|||
|
On Jan 25, 6:15*pm, Ben Bacarisse <ben.use...@bsb.me.uk> wrote:
> Joshua Maurice <joshuamaur...@gmail.com> writes: > | * > | * * 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? |
|
|||
|
On Jan 25, 7:59 pm, Keith Thompson <ks...@mib.org> wrote:
> James Kanze <james.ka...@gmail.com> writes: > > On Jan 24, 11:44 pm, Joshua Maurice <joshuamaur...@gmail.com> wrote: > [...] > >> 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? > Do you mean the lifetime of a POD *object*? Yes. > > 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. 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. -- James Kanze |
|
|||
|
On Jan 26, 2:15 am, Ben Bacarisse <ben.use...@bsb.me.uk> wrote:
> Joshua Maurice <joshuamaur...@gmail.com> writes: > > On Jan 25, 1:35 pm, "Johannes Schaub (litb)" <schaub-johan...@web.de> > > wrote: > >> Joshua Maurice wrote: > >> > 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. 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). -- James Kanze |
|
|||
|
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). |
|
|||
|
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 |
|
|||
|
James Kanze <james.kanze@gmail.com> writes:
> On Jan 26, 2:15 am, Ben Bacarisse <ben.use...@bsb.me.uk> wrote: >> Joshua Maurice <joshuamaur...@gmail.com> writes: >> > On Jan 25, 1:35 pm, "Johannes Schaub (litb)" <schaub-johan...@web.de> >> > wrote: >> >> Joshua Maurice wrote: >> >> > 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. > > 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. -- Ben. |
|
|||
|
Joshua Maurice <joshuamaurice@gmail.com> writes:
[...] > 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. -- Keith Thompson (The_Other_Keith) kst-u@mib.org <http://www.ghoti.net/~kst> Nokia "We must do something. This is something. Therefore, we must do this." -- Antony Jay and Jonathan Lynn, "Yes Minister" |
|
|
![]() |
| Thread Tools | |
| Display Modes | |
|
|