<code>
struct S1 {
size_t type;
// other declarations
};
struct S2 {
size_t type;
// other declarations
};
//snip. an unspecified number of structs S?
struct Sn {
size_t type;
// other declarations
};
typedef union {
struct S1 *s1;
struct S2 *s2;
// a pointer to each struct S? that has been declared
struct Sn *sn;
} cursor_t;
void foo(void)
{
cursor_t cursor;
struct S1 bar;
// init bar
cursor.s1 = &bar;
}
</code>
Does the C language guarantee that cursor.s2->type to cursor.sn->type will refer to bar.type? If so, will
that still be the case if the programmer assigns &bar to cursor.s2 instead of cursor.s1?
Thanks in advance,
Rui Maciel
Assigning &bar to cursor.s2 without a cast is a constraint violation.
The language guarantees that all pointers to struct have the same size
and representation. It also guarantees that there is no padding
before the first member of a struct. But even with a cast, you still
have no guarantee.
It is entirely possible for the alignment of struct S3 to be more
restrictive than the alignment of struct S1. If, for example, struct
S1 consists entirely of int members while struct S3 has a double, then
on many systems a struct S1 object would need to be aligned on a
multiple of four while a struct S3 would need to be aligned on a
multiple of 8. If bar is aligned on an odd multiple of four, then
cursor.s3 contains an invalid value and attempting to dereference it
is probably undefined behavior (as a natural extrapolation of the fact
that evaluating (struct S3*)cursor.s1 is specified as undefined
behavior in the standard).
Remember, if you lie to the compiler, it will get its revenge.
--
Remove del for email
> Assigning &bar to cursor.s2 without a cast is a constraint violation.
> The language guarantees that all pointers to struct have the same size
> and representation. It also guarantees that there is no padding
> before the first member of a struct. But even with a cast, you still
> have no guarantee.
>
> It is entirely possible for the alignment of struct S3 to be more
> restrictive than the alignment of struct S1.
In that case, what about the following code:
<code>
// each type is used to refer to each struct S? declaration
enum s_type {type_1, type_2, ... , type_n};
// snip struct declarations
void foo(void)
{
cursor_t cursor;
struct S1 bar;
// init bar
cursor.s2 = &bar;
switch(cursor.s2->type)
{
case type_1:
// use cursor.s1
break;
case type_2:
// use cursor.s2
break;
// snip
}
}
</code>
In this case, as there is no padding before the first element of the struct and every struct which will be
referenced by cursor has in common the type of it's first element, then will it be possible to verify the
struct type with a union member that doesn't correspond to the passed struct and then, from the value
stored in type, use the corresponding union member?
<snip/>
> Remember, if you lie to the compiler, it will get its revenge.
But this appears to be a very clever lie. The compiler should be amused by such a ruse :D
Rui Maciel
>Barry Schwarz wrote:
>
>> Assigning &bar to cursor.s2 without a cast is a constraint violation.
>> The language guarantees that all pointers to struct have the same size
>> and representation. It also guarantees that there is no padding
>> before the first member of a struct. But even with a cast, you still
>> have no guarantee.
>>
>> It is entirely possible for the alignment of struct S3 to be more
>> restrictive than the alignment of struct S1.
>
>In that case, what about the following code:
>
><code>
>// each type is used to refer to each struct S? declaration
>enum s_type {type_1, type_2, ... , type_n};
>
>// snip struct declarations
>
>void foo(void)
>{
> cursor_t cursor;
> struct S1 bar;
> // init bar
>
> cursor.s2 = &bar;
This is still a constraint violation. Did you mean to use cursor.s1
or make bar a struct S2 or use a cast?
I will assume that somewhere you actually put values in bar.
> switch(cursor.s2->type)
The problem has not gone away. In fact it is unchanged. If any of
the struct S2, S3, ..., Sn has a more demanding alignment than struct
S1 and if bar is not properly aligned for the most restrictive, then
you will have undefined behavior, possibly in the assignment and
switch statements above, and definitely in at least one of the case
groups.
> {
> case type_1:
> // use cursor.s1
> break;
>
> case type_2:
> // use cursor.s2
> break;
>// snip
> }
>}
></code>
>
>In this case, as there is no padding before the first element of the struct and every struct which will be
>referenced by cursor has in common the type of it's first element, then will it be possible to verify the
>struct type with a union member that doesn't correspond to the passed struct and then, from the value
>stored in type, use the corresponding union member?
Not if the value being extracted from the union does meet the
requirements imposed on its type.
If the alignment for struct S1 is 4 and for S2 is 8 and the
address of bar is 0x100004, then your assignment to cursor.s2 invokes
undefined behavior as does your attempt to evaluate cursor.s2->type.
If the alignment for struct S1 and S2 is 4 and for S3 is 8 and
the address of bar is 0x100004, then you will not experience undefined
behavior until you attempt to dereference cursor.s3 (in the implied
case type_3 group).
>
>
><snip/>
>
>> Remember, if you lie to the compiler, it will get its revenge.
>
>But this appears to be a very clever lie. The compiler should be amused by such a ruse :D
Right up until the time you try to test the code in front of your most
important customer. And appearances are often deceiving.
Instead of (possibly in addition to) imbedding your pointers in a
union, why not imbed the structures also in a (probably different)
union and avoid the alignment issues completely.
> On Fri, 02 Oct 2009 23:13:46 +0100, Rui Maciel <rui.m...@gmail.com>
> >struct S1 {
> > size_t type;
> > // other declarations
> >};
> >
> >struct S2 {
> > size_t type;
> > // other declarations
> >};
> >typedef union {
> > struct S1 *s1;
> > struct S2 *s2;
> > // a pointer to each struct S? that has been declared
> > struct Sn *sn;
> >} cursor_t;
> >
> >void foo(void)
> >{
> > cursor_t cursor;
> > struct S1 bar;
> > // init bar
> >
> > cursor.s1 = &bar;
> >}
> >Does the C language guarantee that cursor.s2->type to cursor.sn->type will refer to bar.type? If so, will
> >that still be the case if the programmer assigns &bar to cursor.s2 instead of cursor.s1?
>
> Assigning &bar to cursor.s2 without a cast is a constraint violation.
> The language guarantees that all pointers to struct have the same size
> and representation. It also guarantees that there is no padding
> before the first member of a struct. But even with a cast, you still
> have no guarantee.
Note, however, that if you pull this trick, not with a union of struct
pointers, but with a union of the structs themselves, then, as long as
the initial members are of identical types, it is required to work
(6.5.2.3 #5).
Richard
> Note, however, that if you pull this trick, not with a union of struct
> pointers, but with a union of the structs themselves, then, as long as
> the initial members are of identical types, it is required to work
> (6.5.2.3 #5).
So, considering the following code:
<code>
typedef union {
size_t type; // new, extra-struct type
struct S1 s1;
struct S2 s2;
// a pointer to each struct S? that has been declared
struct Sn sn;
} cursor_t;
int main(void)
{
cursor_t *cursor;
struct S1 bar;
// init bar
cursor = (cursor_t*)&bar;
switch(cursor->type)
{
// perform stuff specific for each struct type
}
return 0;
}
</code>
Would this be valid?
Best regards,
Rui Maciel
> Richard Bos wrote:
>
>> Note, however, that if you pull this trick, not with a union of struct
>> pointers, but with a union of the structs themselves, then, as long as
>> the initial members are of identical types, it is required to work
>> (6.5.2.3 #5).
>
> So, considering the following code:
>
> <code>
> typedef union {
> size_t type; // new, extra-struct type
> struct S1 s1;
> struct S2 s2;
> // a pointer to each struct S? that has been declared
> struct Sn sn;
> } cursor_t;
This is not what Richard was suggesting. You now have a union that
contains /either/ a size_t /or/ one of the struct types. Keep the
type member in the structs and remove it from the union and you have
what Richard proposed.
--
Ben.
If all the structs start with a size_t type, there's no harm in adding it to
the union as a separate alternative. If they don't, you've got a disaster
regardless.
The definition above looks a lot like an XEvent from X11/Xlib.h, which
starts out:
typedef union _XEvent {
int type; /* must not be changed; first element */
XAnyEvent xany;
XKeyEvent xkey;
XButtonEvent xbutton;
...
The code using this union often looks like
/* get a pointer-to-union ev from somewhere */
switch(ev->type) {
case KeyPress: /* do something with ev->xkey */
case KeyPress: /* do something with ev->xkey */
case ButtonRelease: /* do something with ev->xbutton */
case ButtonRelease: /* do something with ev->xbutton */
}
Without the standalone "int type" in the union, you'd have to do the initial
test with one of the structs:
switch(ev->xkey.type) {
case KeyPress: /* do something with ev->xkey */
case KeyPress: /* do something with ev->xkey */
case ButtonRelease: /* do something with ev->xbutton */
case ButtonRelease: /* do something with ev->xbutton */
}
And that just looks weird, peeking into the one member of the union and then
deciding to read from a different one.
--
Alan Curry
I had not considered the possibility that the OP had duplicated rather
than moved the type member. However, I disagree that duplicating it
in the union is harmless. It's harmless if you never use it, yes, but
your example shows that was not what you mean.
> The definition above looks a lot like an XEvent from X11/Xlib.h, which
> starts out:
>
> typedef union _XEvent {
> int type; /* must not be changed; first element */
> XAnyEvent xany;
> XKeyEvent xkey;
> XButtonEvent xbutton;
> ...
>
> The code using this union often looks like
>
> /* get a pointer-to-union ev from somewhere */
> switch(ev->type) {
> case KeyPress: /* do something with ev->xkey */
> case KeyPress: /* do something with ev->xkey */
> case ButtonRelease: /* do something with ev->xbutton */
> case ButtonRelease: /* do something with ev->xbutton */
> }
This is likely to work, yes, but I don't think is it guaranteed to do
what the programmer expects. I think it is permissible for an
implementation to align structs and size_ts in such a way that the
type members don't coincide. That is a practical matter. More
formally, I can't see any text in the standard that assures me that
will work as expected.
> Without the standalone "int type" in the union, you'd have to do the initial
> test with one of the structs:
>
> switch(ev->xkey.type) {
> case KeyPress: /* do something with ev->xkey */
> case KeyPress: /* do something with ev->xkey */
> case ButtonRelease: /* do something with ev->xbutton */
> case ButtonRelease: /* do something with ev->xbutton */
> }
>
> And that just looks weird, peeking into the one member of the union and then
> deciding to read from a different one.
Yes, it looks odd but I think it is needed. You can make it less odd
by having a struct that has only a type member whose name suggests
that its purpose is somehow universal: ev->all_events.type.
--
Ben.
xkey.type is the first member of a struct, so it has the same address as the
struct, which is also the address of the union, which is also the address of
the union's type member. The struct if allocated alone might have a different
alignment requirement than the integer if allocated alone, but the union
has to be aligned for all of its members. Where's the loophole?
--
Alan Curry
6.7.2.1p13 says "A pointer to a structure object, suitably converted,
points to its initial member (or if that member is a bit-field, then to
the unit in which it resides), and vice versa."
However, there's no corresponding wording for unions.
> ... The struct if allocated alone might have a different
> alignment requirement than the integer if allocated alone, but the union
> has to be aligned for all of its members. Where's the loophole?
The standard permits padding before union members, though I know of no
reason why any implementation would take advantage of that option.
6.7.2.1p14: "[...] A pointer to a union object, suitably
converted, points to each of its members (or if a member is a
bitfield, then to the unit in which it resides), and vice versa."
Where does this leave room for (sorry) padding?
(To forestall a possible objection: I do not believe that
"suitably converted" can be used to smuggle in any sleight of
hand like invisibly adjusting the pointer to hide padding.)
--
Eric Sosman
eso...@ieee-dot-org.invalid
OK - now I'm feeling pretty stupid. I looked for that text, failed to
find it despited the fact that it immediately follows 6.7.2.1p13,
which I've already cited. Can I claim that this was due to the
lingering aftermath of the flu I just recovered from? :-(.
> Where does this leave room for (sorry) padding?
>
> (To forestall a possible objection: I do not believe that
> "suitably converted" can be used to smuggle in any sleight of
> hand like invisibly adjusting the pointer to hide padding.)
Unfortunately, due to what I consider a defect in the standard, I do
believe that it can.
It is widely understood that a permissible conversion of a pointer to
a given object results in a new pointer value that points at an object
whose initial byte is the same as the initial byte of the object
pointed at by the original pointer - but the standard never actually
says so, except when the destination type is a pointer to a character
type (6.3.2.3p7).
Using the Rui Maciel's cursor_t, given
cursor_t ct;
Then 6.7.2.1p13 says that (int*)&ct == &ct.type, while 6.3.2.3p7 says
that (char*)&ct.type must point at the first byte of ct.type, and that
(char*)&ct must point at the first byte of ct. It might seem that you
can combine these facts to prove that (char*)&ct and (char*)&ct.type
point at the same byte. However, in order to do that, you have to
assume that (char*)(int*)&ct == (char*)&ct; and the standard provides
no guarantees that this will be the case.
The only time that the standard says anything about the result of
chaining two pointer conversions together is when the second
conversion converts back to the type of the original pointer; in some
of those cases, it guarantees that the result will compare equal to
the original pointer value. None of those cases apply here.
>> So, considering the following code:
>>
>> <code>
>> typedef union {
>>size_t type; // new, extra-struct type
>>struct S1 s1;
>>struct S2 s2;
>>// a pointer to each struct S? that has been declared
>>struct Sn sn;
>> } cursor_t;
>
> This is not what Richard was suggesting. You now have a union that
> contains /either/ a size_t /or/ one of the struct types. Keep the
> type member in the structs and remove it from the union and you have
> what Richard proposed.
Yes, I was aware that the presence of a size_t in the union was never suggested by Richard. Nonetheless, if a
pointer to a union object points to each of it's members and a pointer to a structure object points to it's
initial member then, from the code that I've provided and according to the C standard, shouldn't cursor->type
point to the exact same size_t member of any of the structs' objects being considered?
Rui Maciel
> pac...@kosh.dhis.org (Alan Curry) writes:
>
[snip]
>> The definition above looks a lot like an XEvent from X11/Xlib.h, which
>> starts out:
>>
>> typedef union _XEvent {
>> int type; /* must not be changed; first element */
>> XAnyEvent xany;
>> XKeyEvent xkey;
>> XButtonEvent xbutton;
>> ...
>>
>> The code using this union often looks like
>>
>> /* get a pointer-to-union ev from somewhere */
>> switch(ev->type) {
>> case KeyPress: /* do something with ev->xkey */
>> case KeyPress: /* do something with ev->xkey */
>> case ButtonRelease: /* do something with ev->xbutton */
>> case ButtonRelease: /* do something with ev->xbutton */
>> }
>
> This is likely to work, yes, but I don't think is it guaranteed to do
> what the programmer expects. I think it is permissible for an
> implementation to align structs and size_ts in such a way that the
> type members don't coincide. That is a practical matter. More
> formally, I can't see any text in the standard that assures me that
> will work as expected.
What Alan said -- all members of a union are aligned at the
beginning of the union, and the first member of a struct is
aligned at the beginning of the struct.
>> Without the standalone "int type" in the union, you'd have to do the initial
>> test with one of the structs:
>>
>> switch(ev->xkey.type) {
>> case KeyPress: /* do something with ev->xkey */
>> case KeyPress: /* do something with ev->xkey */
>> case ButtonRelease: /* do something with ev->xbutton */
>> case ButtonRelease: /* do something with ev->xbutton */
>> }
>>
>> And that just looks weird, peeking into the one member of the union and then
>> deciding to read from a different one.
>
> Yes, it looks odd but I think it is needed. You can make it less odd
> by having a struct that has only a type member whose name suggests
> that its purpose is somehow universal: ev->all_events.type.
Why do you think it's needed? Given what the Standard says
(6.7.2.1, among other places) about member alignment in the
two cases, aren't the two objects guaranteed to be in the
same place? Or do you think the access 'ev->type' is suspect
for some other reason?
The very next paragraph covers the union case.
>> ... The struct if allocated alone might have a different
>> alignment requirement than the integer if allocated alone, but the union
>> has to be aligned for all of its members. Where's the loophole?
>
> The standard permits padding before union members, [snip]
Essentially no one other than you believes this. Doesn't
this suggest that how you read the Standard is in need of
reconsideration?
> Eric Sosman wrote:
>> James Kuyper wrote:
>> > [...]
>> > The standard permits padding before union members, though I know of no
>> > reason why any implementation would take advantage of that option.
>>
>> 6.7.2.1p14: "[...] A pointer to a union object, suitably
>> converted, points to each of its members (or if a member is a
>> bitfield, then to the unit in which it resides), and vice versa."
>
> OK - now I'm feeling pretty stupid. I looked for that text, failed to
> find it despited the fact that it immediately follows 6.7.2.1p13,
> which I've already cited. Can I claim that this was due to the
> lingering aftermath of the flu I just recovered from? :-(.
>
>> Where does this leave room for (sorry) padding?
>>
>> (To forestall a possible objection: I do not believe that
>> "suitably converted" can be used to smuggle in any sleight of
>> hand like invisibly adjusting the pointer to hide padding.)
>
> Unfortunately, due to what I consider a defect in the standard, I do
> believe that it can.
>
> It is widely understood that a permissible conversion of a pointer to
> a given object results in a new pointer value that points at an object
> whose initial byte is the same as the initial byte of the object
> pointed at by the original pointer - but the standard never actually
> says so, [except for character types].
What you mean is the Standard never says this directly.
Essentially everyone other than you believes it's implied by
other statements in the Standard. What makes you think your
interpretation is right and all those other people are wrong?
> Using the Rui Maciel's cursor_t, given
>
> cursor_t ct;
>
> Then 6.7.2.1p13 says that (int*)&ct == &ct.type, while 6.3.2.3p7 says
> that (char*)&ct.type must point at the first byte of ct.type, and that
> (char*)&ct must point at the first byte of ct. It might seem that you
> can combine these facts to prove that (char*)&ct and (char*)&ct.type
> point at the same byte. However, in order to do that, you have to
> assume that (char*)(int*)&ct == (char*)&ct; and the standard provides
> no guarantees that this will be the case.
>
> The only time that the standard says anything about the result of
> chaining two pointer conversions together is when the second
> conversion converts back to the type of the original pointer; in some
> of those cases, it guarantees that the result will compare equal to
> the original pointer value. None of those cases apply here.
Again, the only time it says anything directly. The
Standard doesn't always express itself in direct language.
Isn't it more likely that you've misunderstood some
other part of the Standard than that this point has been
missed by everyone else who's looked at it?
I have been unable to identify a valid argument derived from the
actual requirements of the standard to demonstrate that this
conclusion is implied by those requirements. I've discussed that
opinion not just once, but many times, and no one who has disagreed
have ever presented such an argument, either (though not for want of
trying). Do I need anything more than that to justify my conclusion
that no such argument is possible?
What does the number of people who disagree with me have to do with
that?
...
> > The only time that the standard says anything about the result of
> > chaining two pointer conversions together is when the second
> > conversion converts back to the type of the original pointer; in some
> > of those cases, it guarantees that the result will compare equal to
> > the original pointer value. None of those cases apply here.
>
> Again, the only time it says anything directly. The
> Standard doesn't always express itself in direct language.
> Isn't it more likely that you've misunderstood some
> other part of the Standard than that this point has been
> missed by everyone else who's looked at it?
If I had never discussed this issue publicly before, and never seen
the opposing "arguments" before, I might be more willing to consider
that possibility. However, when people with a great deal of knowledge
of the standard, who believe strongly that I'm wrong about this, are
unable to articulate valid arguments based upon correct premises to
support their belief, I think I'm entitled to a little more confidence
in my understanding of this issue than you think I should have.
If you consider that arrogance, so be it.
Honestly I am not yet persuaded. I've been busy so I have not had
time to think this through but the trouble is I am not 100% sure that
the wording in the standard is watertight.
I don't think is makes the assurance you state about alignment, at
least not directly. What it does is say that a pointer, suitably
converted, points to all union members.
I can't quite shake the fear that some peculiar addressing system
allows size_t and structs (even ones that start with a size_t member)
to differently aligned whilst permitting the pointers to work as
required due to the conversion. However, off and on over the last few
days I've tried top come up with a set of mappings between the various
address types that give the effect I am thinking of and I can't!
Every time, the mappings fall foul of some requirement or other or
they simply put the size_t members in the same place (as one would
expect). It is only a nagging doubt that prevents me from saying,
"no, I fold".
<snip>
--
Ben.
> Tim Rentsch wrote:
>> jameskuyper <james...@verizon.net> writes:
> ...
>> > It is widely understood that a permissible conversion of a pointer to
>> > a given object results in a new pointer value that points at an object
>> > whose initial byte is the same as the initial byte of the object
>> > pointed at by the original pointer - but the standard never actually
>> > says so, [except for character types].
>>
>> What you mean is the Standard never says this directly.
>> Essentially everyone other than you believes it's implied by
>> other statements in the Standard. What makes you think your
>> interpretation is right and all those other people are wrong?
>
> I have been unable to identify a valid argument derived from the
> actual requirements of the standard to demonstrate that this
> conclusion is implied by those requirements. I've discussed that
> opinion not just once, but many times, and no one who has disagreed
> have ever presented such an argument, either (though not for want of
> trying). Do I need anything more than that to justify my conclusion
> that no such argument is possible?
Yes, no one has presented an argument that convinces you, I get
that. Have you considered the idea that your convictions rest
on some assumptions that other people generally don't agree
with? Have you ever tried to identify such (possible) assumptions?
> What does the number of people who disagree with me have to do with
> that?
There's a saying which I expect you've heard, "The battle is
not always to the strong, nor the race to the swift. But
that's the way to bet." One person's opinion (or conclusion,
if you prefer that) being different from most other people's
doesn't mean that opinion/conclusion is wrong necessarily,
but it does raise the probability that something is askew.
> ...
>> > The only time that the standard says anything about the result of
>> > chaining two pointer conversions together is when the second
>> > conversion converts back to the type of the original pointer; in some
>> > of those cases, it guarantees that the result will compare equal to
>> > the original pointer value. None of those cases apply here.
>>
>> Again, the only time it says anything directly. The
>> Standard doesn't always express itself in direct language.
>> Isn't it more likely that you've misunderstood some
>> other part of the Standard than that this point has been
>> missed by everyone else who's looked at it?
>
> If I had never discussed this issue publicly before, and never seen
> the opposing "arguments" before, I might be more willing to consider
> that possibility. However, when people with a great deal of knowledge
> of the standard, who believe strongly that I'm wrong about this, are
> unable to articulate valid arguments based upon correct premises to
> support their belief, I think I'm entitled to a little more confidence
> in my understanding of this issue than you think I should have.
Wouldn't other people say the same thing about you? Doesn't
it seem more likely that the two sets of reasonings are based
on different underlying assumptions than that the reasoning
abilities of all those other people are worse than yours?
> If you consider that arrogance, so be it.
Please don't read things into my statements that aren't there.
I didn't use the word arrogance, and in fact the word never
entered my mind. My question was meant only as a question, not
to imply a subtext.
Different people read the Standard in different ways.
I'm interested to learn more about your views here
but first I would like to ask how you read the Standard.
Let me broadly put different ways of reading here into
two categories, (a) "how I think the committee expects
the Standard should be read", and (b) "what the Standard
literally says". The quotes there are not meant as
"scare quotes" but to indicate that these phrases too
have different meanings for different people. (Also,
a point of clarification -- "how I think the committee
expects the Standard should be read" is about the reading
process, not about the conclusions reached.)
Under this somewhat broad classification, which of
the following would you say most closely represents
your position:
(a) "I read the Standard as I think the committee expects
it to be read, and using that reading it isn't clear
that the text mandates the proposed conclusion [that unions
can't have padding at the beginning]";
(b) "It isn't clear how the committee expects the Standard
to be read (perhaps only in some phrasings), and looking
at what the text literally says hasn't convinced me of
the proposed conclusion"; or,
(c) "I don't know (or don't care) how the committee expects
the Standard should be read; what the text literally says
doesn't support the proposed conclusion (or at least I
haven't found passages that provide that support)."
(Or feel free to state an alternative (d) if you can
explain what it is.)
Besides indicating a, b, or c, if you could give section/paragraph
numbers for text you think is relevant to the question at hand,
that would be helpful. More followup after your response.
Yes, but I only considered it briefly; it's seriously inconsistent
with the content of the many discussions I've had on this issue. The
issue seems not to be assumptions that I have made, but assumptions
that others make that I refuse to make. I have considered very
carefully the possibility that other people were right to make those
assumptions; and after careful consideration, rejected that
possibility. Those assumptions are accurate with respect to how real-
world implementations actually implement pointer conversions, and I
quite readily make those assumptions when reasoning about how real-
world implementations work. However, when answering questions about
what the standard actually requires, those assumptions have no basis
in fact. The fundamental problem seems to be getting people to
correctly apply the distinction between what actually happens, and
what the standard requires to happen.
They know how pointer conversions actually work, they read the
standard with that knowledge in place, and see that it's entirely
consistent with their assumptions, they realize that it was clearly
written by authors who made the same assumptions, and they miss the
fact that the standard falls short of actually requiring that those
assumptions be true.
It could have been the case that, despite not explicitly requiring the
assumptions to be true, by making those assumptions sufficiently
frequently, the authors might have accidentally written requirements
that could be combined to derive those requirements implicitly.
However, that hasn't actually happened.
> >> > The only time that the standard says anything about the result of
> >> > chaining two pointer conversions together is when the second
> >> > conversion converts back to the type of the original pointer; in some
> >> > of those cases, it guarantees that the result will compare equal to
> >> > the original pointer value. None of those cases apply here.
> >>
> >> Again, the only time it says anything directly. The
> >> Standard doesn't always express itself in direct language.
> >> Isn't it more likely that you've misunderstood some
> >> other part of the Standard than that this point has been
> >> missed by everyone else who's looked at it?
> >
> > If I had never discussed this issue publicly before, and never seen
> > the opposing "arguments" before, I might be more willing to consider
> > that possibility. However, when people with a great deal of knowledge
> > of the standard, who believe strongly that I'm wrong about this, are
> > unable to articulate valid arguments based upon correct premises to
> > support their belief, I think I'm entitled to a little more confidence
> > in my understanding of this issue than you think I should have.
>
> Wouldn't other people say the same thing about you? ...
That's quite likely. But that's always the case when two people
disagree, pretty much independent of which one is right, or how good
either person's reasons are for believing what they believe.
> ... Doesn't
> it seem more likely that the two sets of reasonings are based
> on different underlying assumptions than that the reasoning
> abilities of all those other people are worse than yours?
If I was unaware of what those assumptions were and how the reasoning
was performed, that would be a very reasonable possibility to
consider. However, I've discussed this many times with many different
people, some of them undoubtedly among the best informed of the people
disagreeing with me on this issue. They have told me what their
assumptions were, and they have told me how they reasoned to reach
that conclusion. I no longer need to guess about those matters; I can
evaluate the accuracy of their assumptions and the quality of their
reasoning - and in every case I've found either one or the other
lacking.
> > If you consider that arrogance, so be it.
>
> Please don't read things into my statements that aren't there.
> I didn't use the word arrogance, and in fact the word never
> entered my mind. My question was meant only as a question, not
> to imply a subtext.
I wasn't reading things into your statements, I was anticipating
possible responses. However, they were only possible, not certain,
which is why I prefaced that comment with "If".
You're implicitly committing the fallacy of an argument from authority
here, with the authority being "everyone other than you", "all those
other people", and "everyone else who's looked at it". However, if I'm
wrong, there should be an actual argument demonstrating that point.
Instead of repeatedly asking me whether I've given adequate
consideration to the possibility that I'm wrong, why don't you just
present the relevant argument?
If "everyone other than you", "all of those other people" and
"everyone else who's looked at it" all know that the assumption I
referred to is correct, then the reasons they have for knowing it
should be fairly well known, too. Please let me know what you think
those reasons are.
Let me give you a little bit of help. The strongest argument I've seen
so far, I've already presented (though my presentation contained a
number of typos no one's bothered commenting on). Given
typedef union {
size_t type; // new, extra-struct type
struct S1 s1;
struct S2 s2;
// a pointer to each struct S? that has been declared
struct Sn sn;
} cursor_t;
cursor_t ct;
Sections 6.3.2.3p7 and 6.7.2.1p14 can be used to demonstrate the
point, which I'll quite happily concede, that
(char*)(size_t*)&ct == (char*)&ct.type
The next step of that argument simplifies this to:
(char*)&ct == (char*)&ct.type
but the person proposing that "proof" could never give me a
justification for that last step which was based upon actual
requirements of the standard. The closest he came was to assume as
true the conclusion that he's trying to prove, and use that assumption
to justify the step.
Care to give it a try? Fix up that argument. Justify the last step in
a non-circular way based upon actual requirements of the standard. Or
continue from that point and go a different way to prove the point.
Alternatively, use your own argument, if you wish.
But please stop questioning whether I've given adequate consideration
of other people's arguments.
> Sections 6.3.2.3p7 and 6.7.2.1p14 can be used to demonstrate the
> point, which I'll quite happily concede, that
> (char*)(size_t*)&ct == (char*)&ct.type
Okay.
> The next step of that argument simplifies this to:
> (char*)&ct == (char*)&ct.type
I think this was intended to be so obvious that no one bothered to specify it.
See, e.g., the language in 6.7.2.1, p13: "There may be padding within a
structure object, but not at its beginning."
Hmm. Okay, let's mess with this. For this NOT to be true, what has to be the
case? It must be that (char *)(size_t *)&ct != (char *)&ct. Which, in turn,
means that there must be some actual fiddling occuring when ct is cast to
(size_t). We know that (char *)&ct points to the lowest byte of ct (6.3.2.3,
parapgrah 7). We know that (size_t *)&ct points to ct.type (6.7.2.1, p.14).
We know that (char *)(size_t *)&ct points to ct.type.
For (char *)&ct not to be the same as (char *)&ct.type, then, there must be
some magic which occurs when &ct is converted to (size_t *), which corresponds
to an internal allocation which places ct.type other than at the beginning
of the union.
I'd actually feel comfortable asserting that the similarity between p13 and
p14 in 6.7.2.1 is intended to guarantee that neither has initial padding.
However, you could argue that the "There may be padding..." sentence being in
p13 and not p14 is an intentional difference, rather than the omission of
a redundant explanation of the meaning of the "suitably converted" language.
Hmm.
Okay, here's a puzzler for you:
(char *)(size_t *)(void *)&ct;
A conversion to (void *) can't itself invoke undefined behavior. Because
of this, we can pass a function in another translation unit a (void *)
which happens to point to ct, and another (void *) which happens to point
to ct.type.
We then convert them both via casts to (size_t *). Since each is a
suitably-converted pointer, they must match. However, there is no way
for the compiler, observing that translation unit, to know that one of
them was a pointer to ct, and one to ct.type. Hmm. Unless (void *) is
sufficiently complicated as to include the pedigree of the pointer, so
that it can continue to transparently shuffle things behind the scenes.
... I think I'll go with: I am pretty sure that it is not possible for
the pointers to differ, and I would have no qualms reporting it as a bug
if they did, and I'd expect it to get fixed. I'm willing to grant that
I'm not totally sure that the standard is explicit, but I think it falls
under the same rule as "all struct pointers smell the same"; we know it's
intended even if it's not explicitly stated.
-s
--
Copyright 2009, all wrongs reversed. Peter Seebach / usenet...@seebs.net
http://www.seebs.net/log/ <-- lawsuits, religion, and funny pictures
http://en.wikipedia.org/wiki/Fair_Game_(Scientology) <-- get educated!
It is precisely my assertion that "There may be unnamed padding within
a structure object, but not at its beginning." cannot be derived from
the previous sentence of the paragraph, and is therefore not
redundant, though I'm quite sure it was intended to be.
...
>
> Hmm.
>
> Okay, here's a puzzler for you:
>
> (char *)(size_t *)(void *)&ct;
>
> A conversion to (void *) can't itself invoke undefined behavior. ...
True. But the behavior, while not unspecified, is underspecified. The
only thing we know for certain about the value resulting from that
conversion is that if it is converted back to cursor_t*, the result of
that conversion would compare equal to ct. The standard says nothing
about where (void*)&ct points, and it says nothing about what happens
when (void*)&ct is converted to any type other than (cursor_t*).
> ... Because
> of this, we can pass a function in another translation unit a (void *)
> which happens to point to ct, and another (void *) which happens to point
> to ct.type.
>
> We then convert them both via casts to (size_t *). Since each is a
> suitably-converted pointer, they must match.
It's precisely at that point which I must disagree. The standard does
not say where (size_t*)(void*)&ct points.
I think it can, but I'm not totally sure. The key is that I don't think
you can get the described behavior (suitably converted pointers point to
first member) without there being no padding.
> True. But the behavior, while not unspecified, is underspecified. The
> only thing we know for certain about the value resulting from that
> conversion is that if it is converted back to cursor_t*, the result of
> that conversion would compare equal to ct. The standard says nothing
> about where (void*)&ct points, and it says nothing about what happens
> when (void*)&ct is converted to any type other than (cursor_t*).
Hmmmm.
Okay, change that to "(char *)&ct", then. That's guaranteed and is
specified explicitly -- it points to the first character of ct.
> It's precisely at that point which I must disagree. The standard does
> not say where (size_t*)(void*)&ct points.
Sure it does. It's a suitably converted pointer to ct, and points to
ct.type, because it's a suitably converted pointer to the union.
As long as there's not an alignment problem, a series of conversions
is well-defined and intermediate conversions don't matter.
So (char *)&ct has to point to the first byte of ct, (char *)&ct.type
has to point to the first byte of ct.type, and (size_t *) of either
has to point to ct.type. Chains of non-undefined conversions have
to work, so far as I can tell. At least, I don't see any exceptions.
If there were padding, the compiler would know what the offset was at
any point where there was a conversion involving the union type. As long
at the type of the union member is not a character type, a conforming
implementation could add that offset when converting from the union type
to the member's type, and subtract it when converting in the opposite
direction. As far as I can see, such an implementation would not violate
any requirement of the standard.
An exception must be made if the member has a character type, because
conversion to a character type is guaranteed to return a pointer to the
first byte of the object. However, there can be padding before any
member of the union that does not have a character type, so long as the
union is big enough to allow that to happen.
>> True. But the behavior, while not unspecified, is underspecified. The
>> only thing we know for certain about the value resulting from that
>> conversion is that if it is converted back to cursor_t*, the result of
>> that conversion would compare equal to ct. The standard says nothing
>> about where (void*)&ct points, and it says nothing about what happens
>> when (void*)&ct is converted to any type other than (cursor_t*).
>
> Hmmmm.
Actually, that was a bad argument on my part. Section 6.2.6.1p4 says
"Values stored in non-bit-field objects of any other object type consist
of n ยด CHAR_BIT bits, where n is the size of an object of that type, in
bytes. The value may be copied into an object of type unsigned char [n]
(e.g., by memcpy);"
Given the interface of memcpy(), that only makes sense if converting a
void* to a char* is guaranteed to give the same result as a direct
conversion to char*. The standard contains no such guarantee explicitly,
but I think that 6.2.6.1p4 implicitly guarantees it.
The key to that conclusion is the fact that memcpy() is not described as
having magical properties that allow it to copy the bytes of an object,
it's merely given as an example of one way to do it; which means that
my_memcpy(), a function with precisely the same interface and the
obvious pure-C implementation of the required semantics for memcpy(),
must serve the same purpose. Therefore, any guarantees that you need to
make my_memcpy() serve that purpose, that aren't already provided
elsewhere in the standard, are arguably implicit in 6.2.6.1p4. In
particular, converting a void* to a char* has to give you the first byte
of the object. This implies that in some sense (void*)&ct must point at
the location in memory of ct, and not be some arbitrary location.
I should have concentrated my attention, not on the (void*)&t
conversion, but the (size_t*). conversion. See below for more details.
...
>> It's precisely at that point which I must disagree. The standard does
>> not say where (size_t*)(void*)&ct points.
>
> Sure it does. It's a suitably converted pointer to ct, and points to
> ct.type, because it's a suitably converted pointer to the union.
The standard unfortunately fails to define what "suitably converted"
means. If (size*)&ct were not suitable, that clause would be too obscure
to be meaningful, but I don't think it's clear that any conversion that
can't be derived from that one qualifies as suitable. If (size*)&ct is
suitable, you can easily derive that (size*)(cursor_t*)(void*)&ct must
be suitable, too. However, it's not clear to me that (size*)(void*)&ct is.
> As long as there's not an alignment problem, a series of conversions
> is well-defined and intermediate conversions don't matter.
That's a common assumption - but is it supported by requirements stated
in the standard? It's certainly not the case for arithmetic conversions:
(float)(int)3.5 is not the same as (float)3.5. Does the standard say
anything explicitly about pointer conversions, that it doesn't say about
arithmetic conversions, that allows you to drop intermediate steps in a
string of pointer conversions? I think it says far less about pointer
conversions than arithmetic ones, and that's precisely the problem.
> So (char *)&ct has to point to the first byte of ct, (char *)&ct.type
> has to point to the first byte of ct.type, and (size_t *) of either
> has to point to ct.type. Chains of non-undefined conversions have
> to work, so far as I can tell. At least, I don't see any exceptions.
I don't know of any real-world exceptions, because every real world
implementation I'm aware of obeys certain common assumptions for which I
can find no support in the standard. That doesn't mean that the standard
actually requires it to work.
That would depend on what "suitably" means.
I am pretty sure that it just means "to the right type, without any
undefined behavior in the meantime".
> An exception must be made if the member has a character type, because
> conversion to a character type is guaranteed to return a pointer to the
> first byte of the object. However, there can be padding before any
> member of the union that does not have a character type, so long as the
> union is big enough to allow that to happen.
Hmm.
(char *)&ct has to point to the first byte of ct.
(char *)&ct.type has to point to the first byte of type.
(size_t *)(char *)&ct has to point, I believe, to the first byte of ct,
as long as (size_t) does not have stricter alignment requirements than
ct.
(size_t *)(char *)&ct.type has to point, I believe, to the first byte
of ct.type.
But there doesn't seem to be any sane way, given the intermediate
(char *), to tell which of the two you have -- so if the (size_t *)
conversion has to be different, there's something implausible
going on.
> Given the interface of memcpy(), that only makes sense if converting a
> void* to a char* is guaranteed to give the same result as a direct
> conversion to char*. The standard contains no such guarantee explicitly,
> but I think that 6.2.6.1p4 implicitly guarantees it.
It's certainly intended, so far as I know.
6.2.5p27 says that (char *) and (void *) have the same representation and
alignment requirements. Hmm. 6.5.4, paragraph 4, "A cast that specifies no
conversion has no effect on the type or value of an expression." I would
argue that if two types have the same representation, that a cast between them
specifies no conversion.
... oh, hey.
I think I'm gonna argue this based on 6.7.2.1p14.
A pointer to a union object, suitably converted, points to *each*
of its members (or if a member is a bitfield, then to the unit in
which it resides) and vice versa.
Emphasis mine.
That same pointer points to each of the members.
Consider, then:
union ct_type {
unsigned char u;
size_t s;
} ct;
Clearly, (unsigned char *) &ct == &ct.u.
(unsigned char *) &ct.s... what about it? Hmm. It clearly, suitably
converted, points to ct. So...
(unsigned char *)(union ct_type *)(unsigned char *)&ct.s == &ct.u
I don't think this allows (unsigned char *) &ct.s to be different
from (unsigned char *) &ct.u.
And since we know that (unsigned char *) &ct has to point to the first
character of ct, and has to compare equal to &ct.u as well, I think that
guarantees that there is no initial padding.
> The standard unfortunately fails to define what "suitably converted"
> means. If (size*)&ct were not suitable, that clause would be too obscure
> to be meaningful, but I don't think it's clear that any conversion that
> can't be derived from that one qualifies as suitable. If (size*)&ct is
> suitable, you can easily derive that (size*)(cursor_t*)(void*)&ct must
> be suitable, too. However, it's not clear to me that (size*)(void*)&ct is.
Hmm. I think malloc'd memory doesn't work if you can't count on that -- you
need to be able to safely copy objects into typeless memory and copy them
out later. Thus, you have to be able to have an (unsigned char *) which
points to a block of data in which you have stashed an arbitrary object,
and no matter what you stashed there, if you cast to that type, you get
the object. So the (unsigned char *) pointer clearly has those properties,
and (void *) has the same representation.
> That's a common assumption - but is it supported by requirements stated
> in the standard? It's certainly not the case for arithmetic conversions:
> (float)(int)3.5 is not the same as (float)3.5. Does the standard say
> anything explicitly about pointer conversions, that it doesn't say about
> arithmetic conversions, that allows you to drop intermediate steps in a
> string of pointer conversions? I think it says far less about pointer
> conversions than arithmetic ones, and that's precisely the problem.
True, it does say less about them, but I think the intent is that pointer
conversions are simply not as complicated.
>Seebs wrote:
>> On 2009-10-09, jameskuyper <james...@verizon.net> wrote:
>>> It is precisely my assertion that "There may be unnamed padding within
>>> a structure object, but not at its beginning." cannot be derived from
>>> the previous sentence of the paragraph, and is therefore not
>>> redundant, though I'm quite sure it was intended to be.
>>
>> I think it can, but I'm not totally sure. The key is that I don't think
>> you can get the described behavior (suitably converted pointers point to
>> first member) without there being no padding.
>
>If there were padding, the compiler would know what the offset was at
>any point where there was a conversion involving the union type. As long
>at the type of the union member is not a character type, a conforming
>implementation could add that offset when converting from the union type
>to the member's type, and subtract it when converting in the opposite
>direction. As far as I can see, such an implementation would not violate
>any requirement of the standard.
>
>An exception must be made if the member has a character type, because
>conversion to a character type is guaranteed to return a pointer to the
>first byte of the object. However, there can be padding before any
>member of the union that does not have a character type, so long as the
>union is big enough to allow that to happen.
I don't think so. 6.5.8p5 says "All pointers to members of the same
union object compare equal." Since pointers always point to the
beginning of the designated object and padding is not part of the
member, it appears that everything must be left aligned at the
beginning of the union.
--
Remove del for email
Ah-hah! There you go. I shoulda thought to look at the equality check.
Actually, I found another piece of evidence: "The value of at most one of the
members can be stored in a union object at any time."
Assume, for the sake of argument, that (void *)(size_t *)&ct != (void *)&ct.
Now, put an unsigned char member in the union, store to the size_t, and
then memcpy a single byte over the unsigned char. It is obvious that both
values still exist, because they're non-overlapping, and that contradicts
the standard's description of how unions work.
So clearly, the assumption was wrong.
The standard does not define the behavior of (T*)(void*)&x, unless T is
either a character type, or the actual type of 'x'. Therefore, I don't
think conversions of this form qualify as 'suitable' (with those two
exceptions).
>> An exception must be made if the member has a character type, because
>> conversion to a character type is guaranteed to return a pointer to the
>> first byte of the object. However, there can be padding before any
>> member of the union that does not have a character type, so long as the
>> union is big enough to allow that to happen.
>
> Hmm.
>
> (char *)&ct has to point to the first byte of ct.
> (char *)&ct.type has to point to the first byte of type.
>
> (size_t *)(char *)&ct has to point, I believe, to the first byte of ct,
> as long as (size_t) does not have stricter alignment requirements than
> ct.
It's not clear to me that the standard actually requires that; but I'll
reserve judgment for now.
> But there doesn't seem to be any sane way, given the intermediate
> (char *), to tell which of the two you have -- so if the (size_t *)
> conversion has to be different, there's something implausible
> going on.
Anything other than the obvious result that we all expect would be
extremely implausible - but my point is, would it be non-conforming?
>> Given the interface of memcpy(), that only makes sense if converting a
>> void* to a char* is guaranteed to give the same result as a direct
>> conversion to char*. The standard contains no such guarantee explicitly,
>> but I think that 6.2.6.1p4 implicitly guarantees it.
>
> It's certainly intended, so far as I know.
>
> 6.2.5p27 says that (char *) and (void *) have the same representation and
> alignment requirements. Hmm. 6.5.4, paragraph 4, "A cast that specifies no
> conversion has no effect on the type or value of an expression." I would
> argue that if two types have the same representation, that a cast between them
> specifies no conversion.
That seems reasonable, but I don't think we can actually apply 6.5.4p4
to anything other than conversion to a type compatible with the
operand's type.
> ... oh, hey.
>
> I think I'm gonna argue this based on 6.7.2.1p14.
>
> A pointer to a union object, suitably converted, points to *each*
> of its members (or if a member is a bitfield, then to the unit in
> which it resides) and vice versa.
>
> Emphasis mine.
>
> That same pointer points to each of the members.
I think that "suitably converted" necessarily means something different
for members of different types, which opens the possibility that, after
suitable conversion, the same pointer may point to different locations
in memory.
> Consider, then:
>
> union ct_type {
> unsigned char u;
> size_t s;
> } ct;
>
> Clearly, (unsigned char *) &ct == &ct.u.
>
> (unsigned char *) &ct.s... what about it? Hmm. It clearly, suitably
> converted, points to ct. So...
> (unsigned char *)(union ct_type *)(unsigned char *)&ct.s == &ct.u
The requirement that conversion to a pointer to a char type points at
the first byte of the object means that my arguments do not apply to
members which have a character type; I'll even concede that this
argument might extend that exemption to other members of the same union;
though it depends upon the poorly defined concept "suitably
converted". Your argument doesn't work, however, if none of the members
has character type.
>> The standard unfortunately fails to define what "suitably converted"
>> means. If (size*)&ct were not suitable, that clause would be too obscure
>> to be meaningful, but I don't think it's clear that any conversion that
>> can't be derived from that one qualifies as suitable. If (size*)&ct is
>> suitable, you can easily derive that (size*)(cursor_t*)(void*)&ct must
>> be suitable, too. However, it's not clear to me that (size*)(void*)&ct is.
>
> Hmm. I think malloc'd memory doesn't work if you can't count on that -- you
> need to be able to safely copy objects into typeless memory and copy them
> out later. Thus, you have to be able to have an (unsigned char *) which
> points to a block of data in which you have stashed an arbitrary object,
> and no matter what you stashed there, if you cast to that type, you get
> the object. So the (unsigned char *) pointer clearly has those properties,
> and (void *) has the same representation.
I don't see a problem for malloc(). I believe that it's permissible for
(void*)&ct to point to one location in memory, and (size_t*)&ct to
point into a different location, which is the actual location where
ct.type is stored. (size*)(void*)&ct, in this case, would point to a
size_t-sized piece of memory at the start of ct, but not at the actual
location of ct.type. I don't see this as causing a problem for malloc.
OK, that's what I was looking for; I didn't find it because I was
expecting the relevant clause to be part of the section describing
unions, or types (6.7.2.1), not in the section describing the relational
operators (had I had thoughts in that direction, I would have expected
it to be in the equality operators section).
Unlike the similar guarantee for members and the union they are part of,
this rule can apply to pointers to two completely unrelated types, which
makes it infeasible to assume that the type conversion needed to allow
them to be comparable might include a shift in location.
Point conceded.
I don't think so, because the possibility of converting them all to
(unsigned char *) and then converting them back means that, if there is a
conversion from (unsigned char *) to (size_t *), it has to be the same
conversion in ALL cases.
And that means that if you do it on the address of a size_t, it has to do the
same thing that it would do on the address of a union which contains a size_t.
So the first byte of the size_t has to be the first byte of the union.
But someone else got us with the relational operator rules, which don't
impose any requirement of "suitably converted".
> The requirement that conversion to a pointer to a char type points at
> the first byte of the object means that my arguments do not apply to
> members which have a character type; I'll even concede that this
> argument might extend that exemption to other members of the same union;
> though it depends upon the poorly defined concept "suitably
> converted". Your argument doesn't work, however, if none of the members
> has character type.
Yeah, I see the issue with 'suitably converted'. I am pretty sure that
that language exists only to handle the "but how can it point to a size_t
if it's not a size_t *" question.
>> Hmm. I think malloc'd memory doesn't work if you can't count on that -- you
>> need to be able to safely copy objects into typeless memory and copy them
>> out later. Thus, you have to be able to have an (unsigned char *) which
>> points to a block of data in which you have stashed an arbitrary object,
>> and no matter what you stashed there, if you cast to that type, you get
>> the object. So the (unsigned char *) pointer clearly has those properties,
>> and (void *) has the same representation.
> I don't see a problem for malloc(). I believe that it's permissible for
> (void*)&ct to point to one location in memory, and (size_t*)&ct to
> point into a different location, which is the actual location where
> ct.type is stored. (size*)(void*)&ct, in this case, would point to a
> size_t-sized piece of memory at the start of ct, but not at the actual
> location of ct.type. I don't see this as causing a problem for malloc.
Consider:
unsigned char *p = malloc(sizeof(union ct_type));
union ct_type *ctp = p;
(unsigned char *)ctp == p.
(size_t *)p == (size_t *)ctp;
At this point, p is a pointer without any magic attachments saying that
it points to a union, so converting it to (size_t *) has to yield the
beginning of the malloc'd space (which is suitably aligned). And that
has to be the same as the address of ctp->size.
Basically, the purpose of using malloc here is to demonstrate that there
can't be anything magical about the pointer. We could argue that the address
of an object of type (union ct_type *) had magical properties controlling
how it is converted when converted to (size_t *), but that can't be true
for the address that came back from malloc.
> OK, that's what I was looking for; I didn't find it because I was
> expecting the relevant clause to be part of the section describing
> unions, or types (6.7.2.1), not in the section describing the relational
> operators (had I had thoughts in that direction, I would have expected
> it to be in the equality operators section).
Me too!
> Point conceded.
I am still semi-convinced that the case can be made without this, but I
will certainly grant that you are right -- it is not nearly as unambiguous
as it is obvious. :)
> Tim Rentsch wrote:
>> [snip]
> [snip]
I did have some explanations and other responses to offer in
reply to this posting (and also a related nephew posting).
However, since later in the thread James took a different
position on the question it seems best not to put these
up and just leave things here. Anyone who is still
interested in those comments is welcome to email me.