0, NULL and variadic functions

  • Thread starter Jens Thoms Toerring
  • Start date
J

Jens Thoms Toerring

Hi,

there's a very recent thread about 0 being an integer constant
and a null pointer constant at the same time (with or without
context). Not being a language lawyer (yet;-) and having some
difficulties following some of the arguments I decided to start
a new thread instead of chiming in on the other one.

My question is: is it necessary to cast 0 to '(void *) 0' when
used as one of the arguments to a variadic function? I have
seen a number of rather convincing arguments for and against
the necessity to do so and the other thread finally got me
sufficently confused to ask...

So, if you have a variadic function called e.g. like this

va_func( "%d%p%d", -1, 0, -1 );

where the "format string" tells the function (like for printf())
to expect an int, a void pointer and then again an int, is this
guaranteed to always work correctly?

The thing I am concerned about is the third argument, '0'. I do
not see yet how the compiler can deduce from the context (or even
without context?) that a (NULL) pointer is meant here. And if the
size of a pointer and that of an int differs (or if the processor
has dedicated data and address registers that might be used for
passing the arguments to the function), how can it figure out what
(or how) exactly to pass to the function? Is the compiler actually
supposed to produce code that can't go wrong or would one have to
write

va_func( "%d%p%d", -1, NULL, -1 );

to make sure there can't be any problems? And are there any
significant differences between C89 and C99 in this respect?

Best regards, Jens
 
S

Stephen Sprunk

Jens said:
there's a very recent thread about 0 being an integer constant
and a null pointer constant at the same time (with or without
context). Not being a language lawyer (yet;-) and having some
difficulties following some of the arguments I decided to start
a new thread instead of chiming in on the other one.

My question is: is it necessary to cast 0 to '(void *) 0' when
used as one of the arguments to a variadic function? I have
seen a number of rather convincing arguments for and against
the necessity to do so and the other thread finally got me
sufficently confused to ask...

So, if you have a variadic function called e.g. like this

va_func( "%d%p%d", -1, 0, -1 );

where the "format string" tells the function (like for printf())
to expect an int, a void pointer and then again an int, is this
guaranteed to always work correctly?

Nope. In that example, 0 will be passed as an int, not as a void*.

Your implementation may be advanced enough to catch that mistake with
*printf(), but it's unlikely to be able to do so for other variadic
functions.
The thing I am concerned about is the third argument, '0'. I do
not see yet how the compiler can deduce from the context (or even
without context?) that a (NULL) pointer is meant here. And if the
size of a pointer and that of an int differs (or if the processor
has dedicated data and address registers that might be used for
passing the arguments to the function), how can it figure out what
(or how) exactly to pass to the function? Is the compiler actually
supposed to produce code that can't go wrong or would one have to
write

va_func( "%d%p%d", -1, NULL, -1 );

to make sure there can't be any problems? And are there any
significant differences between C89 and C99 in this respect?

That isn't guaranteed to work either. NULL might be #defined as 0, in
which case it becomes the same problem as above. If it is #defined as
(void*)0, it will work.

The only two calls that are guaranteed to work are:

va_func( "%d%p%d", -1, (void*)0, -1 );

and

va_func( "%d%p%d", -1, (void*)NULL, -1 );


However, don't be surprised if you see a lot of code that simply passes
NULL. Many modern implementations pass integers and pointers the same
way and/or #define NULL to be (void*)0, so this error is unlikely to be
fixed unless the code has been ported to an unusual implementation that
does neither, exposing the bug.

S
 
K

Kaz Kylheku

Hi,

there's a very recent thread about 0 being an integer constant
and a null pointer constant at the same time (with or without
context).

This is not exactly right, because there is always a context. But it is true
that it is both at the same time. But that is precisely why it is not a
pointer! An expression cannot have two conflicting types at once. It cannot be
both an integer constant with value zero, and a null pointer.

C is a statically typed language, and these types are not in subtype-supertype
relationship in any sense of the word.

The way out of this dilemma is this: a null pointer constant (of this kind) is
/not/ actually a pointer. Something can be a null pointer constant, without
having pointer type. (Language law alert!)

Read the text properly:

An integer constant expression with the value 0, or such an expression cast
to type void *, is called a null pointer constant. If a null pointer
constant is converted to a pointer type, the resulting pointer, called a null
pointer, is guaranteed to compare unequal to a pointer to any object or
function.

This tells us some things.

- Firstly, an integer constant expression with the value 0 is really an integer
constant expression with the value 0.

- The expression (void *) 0 is a null pointer based on a null pointer
constant. Why? Because the 0 by itself is a null pointer constant.
So this null pointer constant is in fact a null pointer constant being cast
to pointer type, but at the same time, this combination is ``blessed''
as also being a null pointer constant. So (void *) 0 does have pointer type.
In fact, it has type void *. If for instance you pass (void *) 0 as a
variadic argument, you are passing a void * null pointer value.

- The fact that (void *) 0 is a null pointer constant, is a special syntactic
and sematnic feature. This ``blessing'' allows (void *) 0 to be converted to
and compared to other pointer types. This is not true of just any void *
pointer, consider:

void *np = 0; // define np to be a null pointer of type void

int (*pf0)(void) = np; // wrong; void * to function pointer conversion

int (*pf1)(void) = (void *) 0; // okay, null pointer constant use!

- A null pointer constant may have to be /converted/ to pointer type in
order to create a null pointer which is unequal to any pointer to a function
or object. I.e. a null pointer isn't /necessarily/ a null pointer, unless
converted to one. Obviously (void *) 0 is a null pointer without needing
to be converted to noe. But 0 isn't a null pointer. The null pointer
constant 0 only behaves as a null pointer in a situation where it is converted
to a pointer. Like when it appears, in an equality comparison, opposite
to a pointer operand. Or when it is used to intialize an object, or
assign a value to it. Etc. It is still a null pointer constant in /other/
contexts, but in those contexts, it behaves as integral expression that
has value 0. It's ``null pointer constancy blessing'' is irrelevant in those
other contexts where it is not converted.
Not being a language lawyer (yet;-) and having some
difficulties following some of the arguments I decided to start
a new thread instead of chiming in on the other one.

My question is: is it necessary to cast 0 to '(void *) 0' when
used as one of the arguments to a variadic function?

It is necessary if the argument is going to be extracted as a pointer.
If the 0 is not converted to a pointer, then it just becomes an argument of
type int with value 0.

So then if va_arg(list, void *) is used (for example), you have a mismatch.

You've forced a four-prong 220 volt plug into a two-prong 110 volt outlet,
except that it was easy, like sliding a hot knife into a block of butter.

So in this situation, the cigar is really just a cigar. The 0 is really just 0.
It's ``null pointer constitude'' is not taken into account.
Is the compiler actually
supposed to produce code that can't go wrong or would one have to
write

va_func( "%d%p%d", -1, NULL, -1 );

But this doesn't help, because NULL is only a preprocessor symbol, which may
expand to either kind of null pointer constant: the kind with the (void *)
cast, and the one that is just an integral expression! NULL expands to an
``implementation-defined null-pointer constant''.

Nowadays I recommend new C programmers not to use NULL in newly designed code
(but to know everything about it, of course).

There are good reasons to avoid NULL. One is that it can lure you into
thinking that it's a pointer, which it isn't.

Another is that it's just a macro; why use a preprocessor macro when you can
use an actual language feature like a built-in constant for the same purpose,
which is also shorter to type?

(What kind of macro expands to something that may be four times shorter than
its invocation? That's a travesty of the word macro, which comes from the idea
of something small and simple expanding to something larger and more
complicated).

Another reason is that you have to include a header file to obtain NULL. If yo
uare working with pointers in some code that doesn't have any header file, you
have to at the very least add #include <stddef.h> to get NULL. You need to add
nothing to get 0.

(Do use NULL in exiting code where it is already used, to preserve its style,
and observe coding conventions).

There is a ghost of a chance that if you write 0 as an argument to a variadic
function, a light bulb will go on in your head that it's not really a pointer,
because it's not being converted to one.

See when it comes to using 0 as a way of getting a null pointer, always think
of it as an integer, and think of whether the given situation does in fact
convert it to a null pointer. When you see 0, do not see ``null pointer''.
Think of the ``null pointer constancy blessing'' that as being a special
/bonus/ behavior which allows you to do nice things like:

if (p != 0) ...

const char *s = 0;

Always remember that this nice syntax entails situations in which a conversion
is being done from 0 to pointer type, which it does not have! No conversion, no
null pointer, just an integer.

Your hacking days will then be happy; you will be unlikely to ever pass 0 as a
pointer to a variadic function.
to make sure there can't be any problems? And are there any
significant differences between C89 and C99 in this respect?

No.
 
N

Nate Eldredge

Hi,

there's a very recent thread about 0 being an integer constant
and a null pointer constant at the same time (with or without
context). Not being a language lawyer (yet;-) and having some
difficulties following some of the arguments I decided to start
a new thread instead of chiming in on the other one.

My question is: is it necessary to cast 0 to '(void *) 0' when
used as one of the arguments to a variadic function?

Yes, if you expect it to be interpreted as a null pointer to void.
I have
seen a number of rather convincing arguments for and against
the necessity to do so and the other thread finally got me
sufficently confused to ask...

So, if you have a variadic function called e.g. like this

va_func( "%d%p%d", -1, 0, -1 );

where the "format string" tells the function (like for printf())
to expect an int, a void pointer and then again an int, is this
guaranteed to always work correctly?

No. On my machine, in fact, it won't.

0 is an integer constant and will be passed like an int. On my machine,
that's a different size from `void *'.
The thing I am concerned about is the third argument, '0'. I do
not see yet how the compiler can deduce from the context (or even
without context?) that a (NULL) pointer is meant here. And if the
size of a pointer and that of an int differs (or if the processor
has dedicated data and address registers that might be used for
passing the arguments to the function), how can it figure out what
(or how) exactly to pass to the function?

It can't. It is up to the programmer to provide arguments of the
appropriate type, by casting if necessary.
Is the compiler actually
supposed to produce code that can't go wrong or would one have to
write

va_func( "%d%p%d", -1, NULL, -1 );

to make sure there can't be any problems?

Actually, even that is not correct.

NULL is allowed to be defined as either "(void *)0" or just "0". If
it's the latter, this example will have the same problem as your first
example. So you should write either

va_func( "%d%p%d", -1, (void *)0, -1 );

or

va_func( "%d%p%d", -1, (void *)NULL, -1 );

whichever you like better.

Note also that if the function you call is expecting a pointer type
other than `void *', you will need to cast your null pointer to that
type. A typical example is the Unix function `execl', which is
prototyped as

int execl(const char *path, const char *arg0, ...);

and is intended to be called with several `char *' arguments giving the
arguments to the program to be executed, the last of which should be a
null pointer.

It should be called as

execl("path", "arg0", "arg1", "arg2", (char *)0);

or

execl("path", "arg0", "arg1", "arg2", (char *)NULL);

But all of the following are wrong:

execl("path", "arg0", "arg1", "arg2", 0);
execl("path", "arg0", "arg1", "arg2", (void *)0); /* I think */
execl("path", "arg0", "arg1", "arg2", NULL);

I recently spent quite some time fixing bugs of this nature in xdvi, a
viewer for DVI files, which made it fail occasionally when run on my
amd64 system.

Incidentally, gcc has a mechanism to tell the compiler about this sort
of "sentinel" convention, so that it can warn if you get it wrong. It requires
that the system headers contain some gcc-specific stuff to invoke this
mechanism, and that the programmer compiles with -Wall (or at least -Wformat).
And are there any
significant differences between C89 and C99 in this respect?

AFAIK, no.

C99 describes this process in 6.5.2.2, by the way.
 
K

Keith Thompson

Kaz Kylheku said:
This is not exactly right, because there is always a context. But
it is true that it is both at the same time. But that is precisely
why it is not a pointer! An expression cannot have two conflicting
types at once. It cannot be both an integer constant with value
zero, and a null pointer.
C is a statically typed language, and these types are not in
subtype-supertype relationship in any sense of the word.
The way out of this dilemma is this: a null pointer constant
(of this kind) is /not/ actually a pointer. Something can be a
null pointer constant, without having pointer type. (Language
law alert!)

A couple of points.

1. The integer constant 0 is specifically of type int; you didn't
mention that.

2. It's true that an expression cannot have two different types, but
implicit conversions can muddy the waters a bit.

I'll illustrate using types int and double, just to avoid any
confusion from dealing with null pointer constants.

double x;
x = (double)42;

On the right hand side of the assignment expression, the subexpression
``42'' is of type int, and the subexpression ``(double)42'' is of type
double. No ambiguity there.

But consider this:

double x;
x = 42;

Again, ``42'' is of type int -- but you can't directly assign an int
to a double, so there has to be an implicit conversion. But if you
look at C99 6.5.16.1, the conversion is performed *by the assignment*.

When I started writing this, I was going to say that there's
implicitly an expression of type double on the RHS of the assignment.
But in fact there isn't (good thing I checked the standard before
posting). The assignment has two operands, an lvalue of type double
and an expression of type int. Again, the conversion from int to
double is performed by the assignment operation.

Similar implicit conversions are performed by initialization, argument
passing, and various other operators.

Similarly, given:

int *ptr;
ptr = 0;

the RHS has type int and value 0. The semantics of the assignment
operator specify that this value is converted to int*; C99 6.3.2.3
specifies how this conversion is done.
 
J

Jens Thoms Toerring

Hi,

thanks everybody for their detailed answers! I have to
think about them carefully and may later come up with some
more questions. At least I now understand what I have to do
to be on the safe side, I just still have to fully grok the
explanations;-)
Best regards, Jens
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
474,263
Messages
2,571,062
Members
48,769
Latest member
Clifft

Latest Threads

Top