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.

chqrlie
  • 131,814
  • 10
  • 121
  • 189
Edgar Rokjān
  • 17,245
  • 4
  • 40
  • 67

2 Answers2

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.

Community
  • 1
  • 1
Nate Eldredge
  • 48,811
  • 6
  • 54
  • 82
  • 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. – too honest for this site Jan 30 '16 at 21:42
  • If `NULL` is defined as `void*(0)`, then the first call is actually correct as you are allowed to pass `void*` for possibly qualified `char*` and vice versa. – fuz Jan 30 '16 at 21:42
  • @FUZxxl: Right, I clarified that. It's not "correct" in the sense of being standard portable C, since it won't be guaranteed to work right if `NULL` is `0`. – Nate Eldredge Jan 30 '16 at 21:45
  • 2
    @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. – fuz Jan 30 '16 at 21:47
  • So, as I understood from your nice explanation, when we just pass a `0` to `foo()` compiler has no reasons to deal with this `0` as with pointer and we just get `const char* str` filled with zeroes (because integer `0` is casted to `const char*`). In this case `str` may not coincide with the representation of null pointer. But when we cast `0` to `void*` compiler decides that `0` is used in pointer context and it should treat it as a null pointer constant and set `str` to null pointer. Am I right? – Edgar Rokjān Jan 30 '16 at 22:32
  • 2
    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... – Nate Eldredge Jan 30 '16 at 22:37
  • 2
    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. – Nate Eldredge Jan 30 '16 at 22:39
  • 2
    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. – Nate Eldredge Jan 30 '16 at 22:41
  • Yes, I understand now. Using the term *cast* was wrong. – Edgar Rokjān Jan 31 '16 at 09:28
  • So, if we call `foo()` with `(void*)0` compiler treats `0` as null pointer constant and put on the stack the real address corresponding to null pointer. Then we just fill `const char*` with this address inside `foo()`, `str` becomes equal to null pointer and correctly compared with `NULL`. Is this logic correct? – Edgar Rokjān Jan 31 '16 at 09:36
  • The last thing I can't figure out is that, as far as I know, null pointer can have different bit representations for different types. In this example we put on stack a `void*` null pointer, but inside `foo()` we need a `char*` null pointer. What are you thinking about it? – Edgar Rokjān Jan 31 '16 at 17:52
  • As far as I know, the C standard requires this code to work, so a compiler that used such representations would have to figure out a way to make it work, For instance, it might convert every pointer passed to a variadic function to `void *`, and then implement `va_arg` in such a way as to convert it appropriately to the type that was requested. – Nate Eldredge Jan 31 '16 at 18:09
  • Thanks for this explanation. I really appreciate your help! – Edgar Rokjān Jan 31 '16 at 22:32
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.

fuz
  • 88,405
  • 25
  • 200
  • 352
  • 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 ;( – Edgar Rokjān Jan 30 '16 at 22:05
  • 1
    @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. – fuz Jan 30 '16 at 22:12
  • 1
    @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. – Keith Thompson Jan 31 '16 at 00:39
  • Thanks for the detailed explanation! – Edgar Rokjān Jan 31 '16 at 22:33
  • @EdgarRokyan It's a pleasure to me. – fuz Jan 31 '16 at 23:18