10

Consider the following code:

#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>

void foo(const char *arg, ...) {
    va_list args_list;

    va_start(args_list, arg);

    for (const char *str = arg;
         str != NULL;
         str = va_arg(args_list, const char *)) {
        printf("%s\n", str);
    }

    va_end(args_list);
}

int main(int argc, char **argv) {
    foo("Some", "arguments", "for", "foo", NULL);
    foo("Some", "arguments", "for", "foo", 0);
    return 0;
}

As we can see, foo() uses variable arguments list to get a list of strings and then print them all. It's supposed that the last argument is null pointer, so arguments list is processed until NULL is detected.

Function foo() is called from main() in two different ways, with NULL and 0 as the last argument.

My question is: is the second call with 0 as the last argument is correct?

I suppose, that we shouldn't call foo() with 0. The reason is that in this case, for example, compiler can't guess from the context that 0 should be treated as null pointer. So it processes it as a usual integer. Then foo() deals with 0 and cast it to const char*. The magic begins when null pointer has internal representation different from 0. As I can understand it leads to the failure in check str != NULL (because str will be equal to 0 casted to const char* which differs from null pointer in our situation) and wrong program behavior.

Are my thoughts right? Any good explanation is appreciated.

2 Answers 2

9

Both calls are incorrect, in general.

The call with bare 0 is certainly incorrect, but not for the reason that you state. When compiling the call to the function foo() which has variable arguments, the compiler has no way to know what type foo() is expecting.

If it were to cast 0 to a const char *, that would be fine; even if the null pointer has internal representation different from all-bits-zero, the language guarantees that using the value 0 in a pointer context results in a null pointer. (This may require the compiler to actually generate some non-trivial code for the typecast, but if so, it's required to do that.)

But it has no reason to think 0 is intended to be a pointer at all. What will happen instead is that it will pass 0 as an int. And this can cause a problem if int has a different size from a pointer, or if for any other reason the int 0 has a different representation than a null pointer, or if this system passes pointer arguments in a different way from integers.

So this is undefined behavior: foo uses va_arg to get an argument of type const char * that was actually passed as type int.

What about using NULL? According to this answer and references therein, the C standard allows the macro NULL to be defined as simply 0 or any other "integer constant expression with the value 0". Contrary to popular belief, it doesn't have to be (void *)0, though it might be.

So it is not safe to pass a bare NULL, because you might be on a platform where it is defined as 0. And then your code could fail for the same reason as above.

To be safe and portable, you can write either of:

 foo("Some", "arguments", "to", "foo", (const char *)0);

or

 foo("Some", "arguments", "to", "foo", (const char *)NULL);

But you can't leave off the cast.

Sign up to request clarification or add additional context in comments.

13 Comments

You are right. Just note additional platform standards might require #define NULL ((void *)0), thus this was well safe. Also, a cast to void * is sufficient.
@NateEldredge I just checked, POSIX requires that NULL be defined as the integer constant 0 casted to void*, so the first call is correct on all POSIX-like systems.
The second part is right, but the first part can be even worse. Imagine a platform with 32-bit int and 64-bit pointers that passes arguments on the stack, and for which the null pointer is all bits 0. When you just pass 0, the compiler pushes a 32-bit 0 onto the stack. But when you call va_arg with const char *, the compiler fetches 64 bits from that address. The other 32 bits could be any old garbage that happened to occupy the next few bytes...
So foo() will get a pointer that is not null, and will not see it as a sentinel. It will try to treat it as a string pointer. But since it contains garbage, the program may crash (or do something else undesirable). I vaguely recall once chasing down a bug where someone called execlp() with last argument 0 and this is exactly what happened.
Thus you aren't even guaranteed to get a const char * filled with zeros. And the integer 0 is never casted to a pointer in that case (which would imply the compiler has the opportunity to transform 0 to an appropriate null pointer). You are simply fetching a pointer from a location where an int was stored, and there are many ways for that to go wrong.
|
8

The second invocation is not correct as you are passing an argument of type int whereas you fetch an argument of type const char* with va_arg. This is undefined behaviour.

The first invocation is only correct if NULL is declared as (void*)0 or similar. Please notice that according to the standard, NULL is merely required to be a null pointer constant. It doesn't have to be defined as ((void*)0) but this is usually the case. Some systems have NULL defined as 0 in which case the first call is undefined behaviour. POSIX mandates that “The macro shall expand to an integer constant expression with the value 0 cast to type void *,” so on a POSIX-like system you can safely assume that NULL is ((void*0).

Here are the relevant standard quotes from ISO 9899:2011 §6.5.2.2:

6.5.2.2 Function calls

(...)

6 If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions. If the number of arguments does not equal the number of parameters, the behavior is undefined. If the function is defined with a type that includes a prototype, and either the prototype ends with an ellipsis (, ...) or the types of the arguments after promotion are not compatible with the types of the parameters, the behavior is undefined. If the function is defined with a type that does not include a prototype, and the types of the arguments after promotion are not compatible with those of the parameters after promotion, the behavior is undefined, except for the following cases:

  • one promoted type is a signed integer type, the other promoted type is the corresponding unsigned integer type, and the value is representable in both types;
  • both types are pointers to qualified or unqualified versions of a character type or void.

7 If the expression that denotes the called function has a type that does include a prototype, the arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type. The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter. The default argument promotions are performed on trailing arguments.

8 No other conversions are performed implicitly; in particular, the number and types of arguments are not compared with those of the parameters in a function definition that does not include a function prototype declarator.

¶8 clarifies that the integer constant 0 is not converted to pointer type when passed for a ... parameter.

5 Comments

Can you explain me in simple words the notion of null pointer constant? I read about null pointer concept, null pointers and null pointer constant but now I stuck a little in this area ;(
@EdgarRokyan A null pointer constant is an integer constant expression with value zero or such an expression cast to type void*. When converted to any pointer type, it yields a null pointer. The null pointer is a pointer guaranteed not to compare equal to any pointer to a function or object.
@FUZxxl: And when not converted to a pointer type, it's of whatever type it's defined as -- possibly int. The constant 0 is a null pointer constant, but it is not of pointer type.
Thanks for the detailed explanation!
@EdgarRokyan It's a pleasure to me.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.