Groups | Search | Server Info | Keyboard shortcuts | Login | Register [http] [https] [nntp] [nntps]
Groups > comp.lang.c > #101454 > unrolled thread
| Started by | Noob <root@127.0.0.1> |
|---|---|
| First post | 2017-01-11 21:26 +0100 |
| Last post | 2017-01-12 13:45 +0000 |
| Articles | 14 — 7 participants |
Back to article view | Back to comp.lang.c
Function pointers conversions and UB Noob <root@127.0.0.1> - 2017-01-11 21:26 +0100
Re: Function pointers conversions and UB "James R. Kuyper" <jameskuyper@verizon.net> - 2017-01-11 15:57 -0500
Re: Function pointers conversions and UB supercat@casperkitty.com - 2017-01-11 14:37 -0800
Re: Function pointers conversions and UB jt@toerring.de (Jens Thoms Toerring) - 2017-01-11 21:46 +0000
Re: Function pointers conversions and UB Keith Thompson <kst-u@mib.org> - 2017-01-11 13:51 -0800
Re: Function pointers conversions and UB "James R. Kuyper" <jameskuyper@verizon.net> - 2017-01-11 17:06 -0500
Re: Function pointers conversions and UB Noob <root@127.0.0.1> - 2017-01-12 00:29 +0100
Re: Function pointers conversions and UB Richard Heathfield <rjh@cpax.org.uk> - 2017-01-11 23:37 +0000
Re: Function pointers conversions and UB supercat@casperkitty.com - 2017-01-11 21:49 -0800
Re: Function pointers conversions and UB Richard Heathfield <rjh@cpax.org.uk> - 2017-01-12 09:41 +0000
Re: Function pointers conversions and UB Noob <root@127.0.0.1> - 2017-01-12 10:52 +0100
Re: Function pointers conversions and UB Richard Heathfield <rjh@cpax.org.uk> - 2017-01-12 09:58 +0000
Re: Function pointers conversions and UB Noob <root@127.0.0.1> - 2017-01-12 13:08 +0100
Re: Function pointers conversions and UB Ben Bacarisse <ben.usenet@bsb.me.uk> - 2017-01-12 13:45 +0000
| From | Noob <root@127.0.0.1> |
|---|---|
| Date | 2017-01-11 21:26 +0100 |
| Subject | Function pointers conversions and UB |
| Message-ID | <o564ak$fjf$1@dont-email.me> |
Hello,
Consider the following code:
void foo(struct s *arg) { }
void (*fp)(void *) = foo;
As far as I understand, this does /not/ have UB, because fp is not used
to call the function. Is that correct?
C99 6.3.2.3 p8
"A pointer to a function of one type may be converted to a pointer to a function of another
type and back again; the result shall compare equal to the original pointer. If a converted
pointer is used to call a function whose type is not compatible with the pointed-to type,
the behavior is undefined."
So the following code does have UB, correct?
void foo(struct s *arg) { }
void (*fp)(void *) = foo;
void bar(void) { fp(); } /* nasal demons from fp() */
I assume the behavior will be "it works as expected" on most platforms.
How/where will it blow up?
Platforms with different argument passing for void * and other pointers?
I lack imagination... What else? Pointer representation/encoding?
Regards.
[toc] | [next] | [standalone]
| From | "James R. Kuyper" <jameskuyper@verizon.net> |
|---|---|
| Date | 2017-01-11 15:57 -0500 |
| Message-ID | <711a0535-e8c0-d5a2-10af-b477f750d671@verizon.net> |
| In reply to | #101454 |
On 01/11/2017 03:26 PM, Noob wrote:
> Hello,
>
> Consider the following code:
>
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;
>
> As far as I understand, this does /not/ have UB, because fp is not used
> to call the function. Is that correct?
No, that's a constraint violation. fp has the type void (*)(void*),
while foo is implicitly converted to a pointer to foo with the type
void(*)(struct s*). Those are not compatible types. This problem could
have been avoided by simply inserting a cast:
void (*fp)(void *) = (void(*)(void*))foo;
With that change, there's absolutely nothing wrong with the code (so far).
> C99 6.3.2.3 p8
> "A pointer to a function of one type may be converted to a pointer to a function of another
> type and back again; the result shall compare equal to the original pointer. If a converted
> pointer is used to call a function whose type is not compatible with the pointed-to type,
> the behavior is undefined."
>
> So the following code does have UB, correct?
>
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;
> void bar(void) { fp(); } /* nasal demons from fp() */
>
> I assume the behavior will be "it works as expected" on most platforms.
That depends entirely upon whether what you "expect" is the same as what
that code actually does on that platform. I recommend against having any
specific expectations - if your expectations for a given platform happen
to be right, that will only serve to mislead you into thinking you're
justified in expecting some particular kind of behavior.
> How/where will it blow up?
>
> Platforms with different argument passing for void * and other pointers?
Yes.
> I lack imagination... What else? Pointer representation/encoding?
That's also a possibility. In the most extreme case (and this is
something that comes up on some real-world machines), void* and struct
s* can have different sizes. I hope you can imagine how badly that could
fail.
The fact that the behavior is undefined if your code makes such a call
gives implementations permission to optimize code in ways that depend
upon the assumption that it will not be making such a call. Those
optimizations can malfunction if you actually do make such a call, and
the way in which they malfunction is likely to be a way you couldn't
even imagine.
I don't have any particular optimizations in mind when I write that. I
just know that similar issues with other kinds of undefined behavior
have caused malfunctions in code that the author was certain could not
possibly happen. I would not recommend being certain about this code.
[toc] | [prev] | [next] | [standalone]
| From | supercat@casperkitty.com |
|---|---|
| Date | 2017-01-11 14:37 -0800 |
| Message-ID | <89b8d979-6a89-4836-857f-93d45ba8519f@googlegroups.com> |
| In reply to | #101462 |
On Wednesday, January 11, 2017 at 2:57:52 PM UTC-6, James R. Kuyper wrote: > That depends entirely upon whether what you "expect" is the same as what > that code actually does on that platform. I recommend against having any > specific expectations - if your expectations for a given platform happen > to be right, that will only serve to mislead you into thinking you're > justified in expecting some particular kind of behavior. On the vast majority of platforms, the representations of pointer types are documented, pointers to all kinds of objects have the same representation as identically-qualified pointers to void, and the only qualifiers which affect representation are implementation-defined extensions. On such a platform, there is exactly one sensible behavior if the address of a function accepting a void* is cast to a pointer to a function accepting some other pointer type. The Standard refrains from mandating such behavior because such a mandate would make no sense on platforms where pointer representations were dependent upon the target type, but that doesn't mean that any useful purpose is served by the notion that quality implementations for platforms which specify the use common pointer representations should not also specify behaviors of pointers to functions whose arguments have the same representations as likewise being interchangeable. I'm well aware that it has become fashionable to say that compilers should treat code which has a natural meaning on some platforms but not others as though it would be meaningless on all platforms, and programmers need to be aware of the need to affirmatively block compilers from doing so, but in cases where there is one near meaning for a piece of code, there is no basis for claiming that it's unclear what the programmer would be expecting.
[toc] | [prev] | [next] | [standalone]
| From | jt@toerring.de (Jens Thoms Toerring) |
|---|---|
| Date | 2017-01-11 21:46 +0000 |
| Message-ID | <ednnehFgetrU1@mid.uni-berlin.de> |
| In reply to | #101454 |
Noob <root@127.0.0.1> wrote:
> Consider the following code:
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;
> As far as I understand, this does /not/ have UB, because fp is not used
> to call the function. Is that correct?
> C99 6.3.2.3 p8
> "A pointer to a function of one type may be converted to a pointer to a
> function of another type and back again; the result shall compare equal to
> the original pointer. If a converted pointer is used to call a function
> whose type is not compatible with the pointed-to type, the behavior is
> undefined."
> So the following code does have UB, correct?
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;
> void bar(void) { fp(); } /* nasal demons from fp() */
This call probaly will get the compiler to complain since
you call via 'fp' without an argument eventhough 'fp' is
defined to point to a function that expects one (regard-
less of if it's the wrong type). I assume you forgot e.g.
a 'NULL' argument here.
> I assume the behavior will be "it works as expected" on most platforms.
> How/where will it blow up?
> Platforms with different argument passing for void * and other pointers?
Indeed. A platform definitely may have pointers of different
sizes, where void pointers, of course, must be large enough
to represent pointers of all possible sizes. One, though pu-
rely theoretical, problem might be an architecture with dedi-
cated registers for e.g. small and large pointers. Given a
calling convention that allows for passing arguments via re-
gisters (which is very common), in that case the argument you
call the function via 'fp' with might be passed via a register
for 'long' pointers, but the function expects it in a register
for a 'short' pointer, thus getting some garbage. Or, if 'small'
pointers have just half the size of a 'large' pointer, it may
try to pass two 'small' pointers combined into a register for
a 'large' pointer, shifting one of them to the more signifi-
cant bits. If the calling convention calls for the first poin-
ter passed to always be shifted up (even if there's only one),
the function called will only use the upper half of the regis-
ter, but which, due to being called with a 'long' pointer, con-
tains the "relevant" bits in the lower half.
This is indeed purely theoretical (at least I don't know of
any such architecture), but that kind of problem isn't unknown
on archtiectures with dedicated data and address registers when
you try to use e.g. ints (or longs, whatever fits the size of a
pointer) instead of a pointer. If you'd do something like this
void foo(void *);
void (*fp)(int) = foo;
...
fp(0x1000);
(where 0x1000 might be a perfectly valid address for the
purposes of foo()) the compiler may pass the argument via
a data register while the actual function called may look
for it in an address register - which just contains some
more or less random value.
On that kind of architecture it's also a not too uncommon (and
hard to spot) bug when you have e.g.
/* no include of <stdlib.h> */
int *ip = (int *) malloc(100 * sizeof *ip);
malloc() will return via a address register, but since
there's no declaration of malloc() the compiler assumes
that it returns an int (at least with C89). And thus in
the caller an attempt will be made to retrieve the return
value from a data register and use that (random) value for
'ip' - with the somewhat unintuitive result that the return
value looks like malloc() succeeded (it's not necessarily
NULL), but an attempt to use 'ip' is rather likely to pro-
duce a segmentation fault.
Best regards, Jens
--
\ Jens Thoms Toerring ___ jt@toerring.de
\__________________________ http://toerring.de
[toc] | [prev] | [next] | [standalone]
| From | Keith Thompson <kst-u@mib.org> |
|---|---|
| Date | 2017-01-11 13:51 -0800 |
| Message-ID | <lna8axp8nv.fsf@kst-u.example.com> |
| In reply to | #101454 |
Noob <root@127.0.0.1> writes:
> Hello,
>
> Consider the following code:
>
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;
>
> As far as I understand, this does /not/ have UB, because fp is not used
> to call the function. Is that correct?
It violates a constraint. There is no implicit conversion from one
pointer-to-function type to another.
> C99 6.3.2.3 p8
> "A pointer to a function of one type may be converted to a pointer to a function of another
> type and back again; the result shall compare equal to the original pointer. If a converted
> pointer is used to call a function whose type is not compatible with the pointed-to type,
> the behavior is undefined."
Yes, it "may be converted", but a cast is required.
This is valid and exhibits no undefined behavior:
void foo(struct s *arg) { }
void (*fp)(void *) = (void(*)(void*))foo;
(Presumably `struct s` is defined somewhere; otherwise it's not visible
outside the function definition, and it's going to be difficult to write
a meaningful call.)
> So the following code does have UB, correct?
>
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;
Again, assume a cast.
> void bar(void) { fp(); } /* nasal demons from fp() */
Actually that's another constraint violation -- but
fp(NULL);
is legal (doesn't violate a syntax rule or constraint) and has undefined
behavior.
> I assume the behavior will be "it works as expected" on most platforms.
> How/where will it blow up?
>
> Platforms with different argument passing for void * and other pointers?
>
> I lack imagination... What else? Pointer representation/encoding?
A likely cause of the call not "working as expected" (which actually
depends a great deal on your expectations) is that you've passed
a pointer to an object that isn't properly aligned to point to a
`struct s` object. Differing representations and argument passing
conventions are also possibilities (likely not to apply to most
current systems). And compiler optimization could mess things up.
--
Keith Thompson (The_Other_Keith) kst-u@mib.org <http://www.ghoti.net/~kst>
Working, but not speaking, for JetHead Development, Inc.
"We must do something. This is something. Therefore, we must do this."
-- Antony Jay and Jonathan Lynn, "Yes Minister"
[toc] | [prev] | [next] | [standalone]
| From | "James R. Kuyper" <jameskuyper@verizon.net> |
|---|---|
| Date | 2017-01-11 17:06 -0500 |
| Message-ID | <c4692699-b580-5d0d-a381-dc22675719f3@verizon.net> |
| In reply to | #101470 |
On 01/11/2017 04:51 PM, Keith Thompson wrote:
> Noob <root@127.0.0.1> writes:
...
>> void foo(struct s *arg) { }
>> void (*fp)(void *) = foo;
>
> Again, assume a cast.
>
>> void bar(void) { fp(); } /* nasal demons from fp() */
>
> Actually that's another constraint violation -- but
> fp(NULL);
> is legal (doesn't violate a syntax rule or constraint) and has undefined
> behavior.
Actually, judging from his "works as expected" question, if he had
realized the problem with the missing argument, he might have written
something like the following:
struct s s1;
fp(&s1);
>> I assume the behavior will be "it works as expected" on most platforms.
>> How/where will it blow up?
...
> A likely cause of the call not "working as expected" (which actually
> depends a great deal on your expectations) is that you've passed
> a pointer to an object that isn't properly aligned to point to a
> `struct s` object.
That, at least, wouldn't be a problem with fp(&s1).
[toc] | [prev] | [next] | [standalone]
| From | Noob <root@127.0.0.1> |
|---|---|
| Date | 2017-01-12 00:29 +0100 |
| Message-ID | <o56f1m$njp$1@dont-email.me> |
| In reply to | #101454 |
On 11/01/2017 21:26, Noob wrote:
> Consider the following code:
>
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;
Several of you have pointed out that omitting the cast is
a constraint violation. Are implementations still allowed
to produce object code, after having printed some kind of
warning?
foo.c:3:22: warning: initialization from incompatible pointer type [enabled by default]
void (*fp)(void *) = foo;
^
> As far as I understand, this does /not/ have UB, because fp is not used
> to call the function. Is that correct?
>
> C99 6.3.2.3 p8
> "A pointer to a function of one type may be converted to a pointer to a function of another
> type and back again; the result shall compare equal to the original pointer. If a converted
> pointer is used to call a function whose type is not compatible with the pointed-to type,
> the behavior is undefined."
>
> So the following code does have UB, correct?
>
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;
> void bar(void) { fp(); } /* nasal demons from fp() */
Doh! I did mean fp(0);
> I assume the behavior will be "it works as expected" on most platforms.
"As expected" meant "calling foo with arg=NULL" of course.
(I thought it would be obvious what the expectation was.)
> How/where will it blow up?
>
> Platforms with different argument passing for void * and other pointers?
>
> I lack imagination... What else? Pointer representation/encoding?
I may have oversimplified my example. The actual situation
looks more like atexit. One registers a function that is
supposed to be called later, in specific circumstances.
The actual code looks like this:
struct foo { int i; };
extern struct foo *alloc_foo(void);
extern void api_func(struct foo *arg);
extern void register_func(void (*func)(void *), void *data);
/* register_func saves func and data, to be able to call func(data) later */
void user_code(void) {
struct foo *data = alloc_foo();
register_func((void (*)(void *))api_func, data);
}
A simple solution would be declaring a wrapper around api_func:
void wrapper(void *arg) { api_func(arg); }
and passing the wrapper to register_func.
This is 100% portable, right?
Regards.
[toc] | [prev] | [next] | [standalone]
| From | Richard Heathfield <rjh@cpax.org.uk> |
|---|---|
| Date | 2017-01-11 23:37 +0000 |
| Message-ID | <o56fha$p3m$1@dont-email.me> |
| In reply to | #101488 |
On 11/01/17 23:29, Noob wrote:
> On 11/01/2017 21:26, Noob wrote:
>
>> Consider the following code:
>>
>> void foo(struct s *arg) { }
>> void (*fp)(void *) = foo;
>
> Several of you have pointed out that omitting the cast is
> a constraint violation. Are implementations still allowed
> to produce object code, after having printed some kind of
> warning?
Yes, absolutely. The diagnostic message is a requirement on the
implementation, but it is not under any obligation to stop translation.
<snip>
> I may have oversimplified my example. The actual situation
> looks more like atexit. One registers a function that is
> supposed to be called later, in specific circumstances.
>
> The actual code looks like this:
>
> struct foo { int i; };
> extern struct foo *alloc_foo(void);
> extern void api_func(struct foo *arg);
> extern void register_func(void (*func)(void *), void *data);
> /* register_func saves func and data, to be able to call func(data) later */
> void user_code(void) {
> struct foo *data = alloc_foo();
> register_func((void (*)(void *))api_func, data);
> }
>
>
> A simple solution would be declaring a wrapper around api_func:
>
> void wrapper(void *arg) { api_func(arg); }
>
> and passing the wrapper to register_func.
>
> This is 100% portable, right?
FWIW, I can't see anything wrong with it. It's a shame you can't come up
with a consistent declaration for the registered functions, though, as
it would enable you to avoid an ugly cast.
--
Richard Heathfield
Email: rjh at cpax dot org dot uk
"Usenet is a strange place" - dmr 29 July 1999
Sig line 4 vacant - apply within
[toc] | [prev] | [next] | [standalone]
| From | supercat@casperkitty.com |
|---|---|
| Date | 2017-01-11 21:49 -0800 |
| Message-ID | <ce2dc6d1-a298-4dd5-acc6-d90ab840d97d@googlegroups.com> |
| In reply to | #101489 |
On Wednesday, January 11, 2017 at 5:37:50 PM UTC-6, Richard Heathfield wrote: > Yes, absolutely. The diagnostic message is a requirement on the > implementation, but it is not under any obligation to stop translation. An implementation is required to produce some kind of diagnostic when given any file containing a constraint violation, but for constraint violations not involving the #error directive the Standard doesn't require that the "diagnostic" have anything to do with the violation. A conforming implementation could output "This is a diagnostic" as its sole message when given *any* input, well-formed or otherwise, which does not contain an #error directive.
[toc] | [prev] | [next] | [standalone]
| From | Richard Heathfield <rjh@cpax.org.uk> |
|---|---|
| Date | 2017-01-12 09:41 +0000 |
| Message-ID | <o57itu$f1m$1@dont-email.me> |
| In reply to | #101497 |
On 12/01/17 05:49, supercat@casperkitty.com wrote: > On Wednesday, January 11, 2017 at 5:37:50 PM UTC-6, Richard Heathfield wrote: >> Yes, absolutely. The diagnostic message is a requirement on the >> implementation, but it is not under any obligation to stop translation. > > An implementation is required to produce some kind of diagnostic when > given any file containing a constraint violation, but for constraint > violations not involving the #error directive the Standard doesn't require > that the "diagnostic" have anything to do with the violation. A conforming > implementation could output "This is a diagnostic" as its sole message when > given *any* input, well-formed or otherwise, which does not contain an > #error directive. The implementation doesn't even have to say that the diagnostic message is diagnostic. It could, for example, respond to syntax errors or constraint violations by printing "honi soit qui mal y pense". Or even "translation succeeded"! -- Richard Heathfield Email: rjh at cpax dot org dot uk "Usenet is a strange place" - dmr 29 July 1999 Sig line 4 vacant - apply within
[toc] | [prev] | [next] | [standalone]
| From | Noob <root@127.0.0.1> |
|---|---|
| Date | 2017-01-12 10:52 +0100 |
| Message-ID | <o57jie$h2u$1@dont-email.me> |
| In reply to | #101489 |
On 12/01/2017 00:37, Richard Heathfield wrote:
> On 11/01/17 23:29, Noob wrote:
>
>> I may have oversimplified my example. The actual situation
>> looks more like atexit. One registers a function that is
>> supposed to be called later, in specific circumstances.
>>
>> The actual code looks like this:
>>
>> struct foo { int i; };
>> extern struct foo *alloc_foo(void);
>> extern void api_func(struct foo *arg);
>> extern void register_func(void (*func)(void *), void *data);
>> /* register_func saves func and data, to be able to call func(data) later */
>> void user_code(void) {
>> struct foo *data = alloc_foo();
>> register_func((void (*)(void *))api_func, data);
>> }
>>
>>
>> A simple solution would be declaring a wrapper around api_func:
>>
>> void wrapper(void *arg) { api_func(arg); }
>>
>> and passing the wrapper to register_func.
>>
>> This is 100% portable, right?
>
> FWIW, I can't see anything wrong with it. It's a shame you can't come up
> with a consistent declaration for the registered functions, though, as
> it would enable you to avoid an ugly cast.
I'm confused. There is no ugly cast in my "simple solution".
Which cast are you talking about?
void user_code(void) {
struct foo *data = alloc_foo();
register_func(wrapper, data);
}
Regards.
[toc] | [prev] | [next] | [standalone]
| From | Richard Heathfield <rjh@cpax.org.uk> |
|---|---|
| Date | 2017-01-12 09:58 +0000 |
| Message-ID | <o57jt0$hj1$2@dont-email.me> |
| In reply to | #101503 |
On 12/01/17 09:52, Noob wrote: > On 12/01/2017 00:37, Richard Heathfield wrote: > >> On 11/01/17 23:29, Noob wrote: <snip> >>> register_func((void (*)(void *))api_func, data); <snip> >> [...] an ugly cast. > > I'm confused. There is no ugly cast in my "simple solution". > Which cast are you talking about? That one. -- Richard Heathfield Email: rjh at cpax dot org dot uk "Usenet is a strange place" - dmr 29 July 1999 Sig line 4 vacant - apply within
[toc] | [prev] | [next] | [standalone]
| From | Noob <root@127.0.0.1> |
|---|---|
| Date | 2017-01-12 13:08 +0100 |
| Message-ID | <o57rhu$ioj$1@dont-email.me> |
| In reply to | #101454 |
On 11/01/2017 21:26, Noob wrote:
> void foo(struct s *arg) { }
> void (*fp)(void *) = foo;
C99 6.3.2.3 p8
"A pointer to a function of one type may be converted to a pointer to a function of another
type and back again; the result shall compare equal to the original pointer. If a converted
pointer is used to call a function whose type is not compatible with the pointed-to type,
the behavior is undefined."
6.7.5.3 p15
For two function types to be compatible, both shall specify compatible return types.
Moreover, the parameter type lists, if both are present, shall agree in the number of
parameters and in use of the ellipsis terminator; corresponding parameters shall have
compatible types.
Are "void (*)(void *)" and "void (*)(struct s *)" compatible types?
It boils down to the function parameter:
Are "void *" and "struct s *" compatible types?
I think: "no", but I can't find definitive C&V... unless this?
6.2.7 p1 Compatible type and composite type
"Two types have compatible type if their types are the same. Additional rules for
determining whether two types are compatible are described in 6.7.2 for type specifiers,
in 6.7.3 for type qualifiers, and in 6.7.5 for declarators."
A type has a type? /me brain exploded
Regards.
[toc] | [prev] | [next] | [standalone]
| From | Ben Bacarisse <ben.usenet@bsb.me.uk> |
|---|---|
| Date | 2017-01-12 13:45 +0000 |
| Message-ID | <87pojsqtmx.fsf@bsb.me.uk> |
| In reply to | #101514 |
Noob <root@127.0.0.1> writes: > On 11/01/2017 21:26, Noob wrote: <snip> > Are "void (*)(void *)" and "void (*)(struct s *)" compatible types? > > It boils down to the function parameter: > > Are "void *" and "struct s *" compatible types? > > I think: "no", but I can't find definitive C&V... unless this? Definitely no. Type compatibility is very strict: char is not even compatible with one of signed char and unsigned char. > 6.2.7 p1 Compatible type and composite type > "Two types have compatible type if their types are the > same. Additional rules for determining whether two types are > compatible are described in 6.7.2 for type specifiers, in 6.7.3 for > type qualifiers, and in 6.7.5 for declarators." > > A type has a type? /me brain exploded I think that's a typo that comes from incomplete editing. Maybe it once said something like "[t]wo values have compatible type if their types are the same" and someone decided to focus on the types (since types exists independently of values or objects) and failed to change "their" to "the". tl;dr: type compatibility is exact type equality with a few tweaks. -- Ben.
[toc] | [prev] | [standalone]
Back to top | Article view | comp.lang.c
csiph-web