5

From The Linux Programming Interface

execl(prog, arg, (char *) 0);
execl(prog, arg, (char *) NULL);

Casting NULL in the manner of the last call above is generally required, even on implementations where NULL is defined as (void *) 0.

This is because, although the C standards require that null pointers of different types should test true for comparisons on equality, they don’t require that pointers of different types have the same internal representation (although on most implementations they do).
And, in a variadic function, the compiler can’t cast (void *) 0 to a null pointer of the appropriate type.

The C standards make one exception to the rule that pointers of different types need not have the same representation: pointers of the types char * and void * are required to have the same internal representation. This means that passing (void *) 0 instead of (char *) 0 would not be a problem in the example case of execl(), but, in the general case, a cast is needed.

  1. "Casting NULL in the manner of the last call above is generally required"

    Does the C standard requires the null pointer be represented the same as (char*) 0?

  2. "in a variadic function such as execl(), the compiler can’t cast (void *) 0 to a null pointer of the appropriate type."

    Is (void *) 0 not a null pointer of a type?

    If yes, why can't the compiler cast (void *) 0 in execl(prog, arg, (void*) 0) to "a null pointer of the appropriate type"?

  3. "pointers of the types char * and void * are required to have the same internal representation. This means that passing (void *) 0 instead of (char *) 0 would not be a problem in the example case of execl()".

    Can the compiler cast (void *) 0 in execl(prog, arg, (void*) 0) to "a null pointer of the appropriate type" now?

    Why does it contradict to the quote in my point 2?

  4. If I replace (void *) 0 in execl(prog, arg, (void*) 0) with cast of 0 to any type's pointer, such as (int *) 0, can the compiler cast (int *) 0 in execl(prog, arg, (int*) 0) to "a null pointer of the appropriate type"? Thanks.

  5. For a non-variadic function call, such as in sigaction(SIGINT, &sa, (int*) 0), can the compiler cast (int *) 0 to "a null pointer of the appropriate type"?

Thanks.

DevSolar
  • 67,862
  • 21
  • 134
  • 209
Tim
  • 1
  • 141
  • 372
  • 590

4 Answers4

7

Firstly, the compiler does not "cast" in any circumstance. A cast is a syntax construct in the source code which requests a conversion.

I will assume that when you talk about "the compiler casting" you mean to talk about implicit conversion which is the process whereby a value of one type may be converted to a value of another type, without a cast operator.

The Standard specifies precisely the contexts in which implicit conversion may be applied; there must always be a target type. For example in the code int x = Y; the expression Y can be some type that is not an int, but which has implicit conversion to int defined.

No implicit conversion is applied to function arguments that correspond to the ... part of a prototype, other than the default argument promotions. For pointer values, the default argument promotions leave them unchanged.

A common thread of your question seems to be that the compiler should somehow pretend that execl behaves as if there were a prototype in place for the last argument. But in fact there is not, and the compiler doesn't have any magic behaviour for specific functions. What you pass is what you get.


  1. The standard specifies that the value of the expression (char *)0 is a null pointer. It says nothing about the representation of null pointers, and there may be multiple different representations that are all null pointers.

  2. The execl function specification says that the argument list should be terminated by (char *)0 which is a value of type char *. A value of type void * is not a value of type char * and there are no implicit conversions in this context as discussed above.

  3. There is still no implicit conversion; the text you quote is saying that you can use the wrong type argument in this one specific situation (no prototype parameter; and char * expected but void * provided, or vice versa).

  4. That would be undefined behaviour , the text you quoted in point 3 does not apply to int *.

  5. The sigaction function has a prototype; the parameter in question is struct sigaction *oldact. When you try to initialize a prototype parameter (or any variable) with a value of different type, implicit conversion to the type of the parameter is attempted. There is implicit conversion from any null pointer value to a null pointer value of a different type. This rule is in C11 6.3.2.3/4 . So that code is OK.

M.M
  • 138,810
  • 21
  • 208
  • 365
  • @EricPostpischil They have different types despite being interchangeable as function arguments ... OP's question 2 is not very clear, I'll try to reword my answer to that part – M.M Sep 06 '18 at 02:15
  • 1
    *The standard specifies that the value of the expression `(char *)0` is a null pointer.* Does it? [**6.3.2.3 Pointers**, p3](https://port70.net/~nsz/c/c11/n1570.html#6.3.2.3p3) states "An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. ..." I don't see how `( char * ) 0` fits there. It's an integer constant expression with the value 0, but it's cast to `char *`. – Andrew Henle Sep 06 '18 at 13:31
  • 1
    @AndrewHenle you're mixing up *null pointer constant* with *null pointer*. `(char *)0` is a *null pointer*, and an *address constant*, but not a *null pointer constant* ! – M.M Sep 06 '18 at 22:03
  • That's my point - `(char *)0` not a null pointer constant. The second part of 6.3.2.3p3 starts "If a null pointer constant is converted to a pointer type...". Since `(char *)0` isn't a null pointer constant, how does it get to the point of being a null pointer? I'd say that's not a strictly-conformant way of getting a null pointer, albeit one that's likely to work. Although I'm open to interpretations of it actually being a null pointer, since the intent of that part of the C standard was to validate poor code that assumed 0 is a null pointer. – Andrew Henle Sep 06 '18 at 22:14
  • @AndrewHenle I never claimed `(char *)0` was a null pointer constant. The second sentence of 6.3.2.3/3 explains why it is a null pointer. The null pointer constant `0` is converted to a pointer type which produces a null pointer. – M.M Sep 06 '18 at 22:24
  • Your interpretation requires that a cast be a "conversion". If that's true, the "or such an expression cast to type void *" statement of 6.3.2.3p3 is extraneous, and I'd argue misleading. Why would the standard single out the cast to `void *` if any cast of an integer zero constant expression to a pointer would be a valid null pointer? I can see something like `( int * ) 0` being a valid non-null pointer in architectures such as some of the x86 memory models or Cray word-based integer pointers (IIRC). Admittedly, a (char *) cast might be special in that it has much in common with a void *. – Andrew Henle Sep 06 '18 at 22:57
  • 1
    @AndrewHenle A cast is an explicit conversion. The operand of the cast operator is converted to the type in the parentheses. (C11 6.5.4/5). The text you quote is not extraneous, again you seem to be confusing *null pointer constant* with *null pointer*. `(void *)0` is a null pointer and a null pointer constant. `(char *)0` is a null pointer and not a null pointer constant. – M.M Sep 06 '18 at 23:07
6

Since C99, the specification of va_arg reads in part

If [the type passed to va_arg as an argument] is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined, except for the following cases:

  • one type is a signed integer type, the other type is the corresponding unsigned integer type, and the value is representable in both types;
  • one type is pointer to void and the other is a pointer to a character type.

The second bullet point means that, for any variadic function that uses va_arg to access its arguments, a call of the form

variadic_function("a", "b", "c", (void *)0);

will be valid whenever

variadic_function("a", "b", "c", (char *)0);

would have been.

There is, unfortunately, a catch: I can't find any requirement for variadic standard library functions1 to [behave as-if they] access their arguments by making a series of calls to va_arg. You're probably thinking, well, how else are they going to do it? In practice it's va_arg or hand-written assembly language, and maybe the committee didn't want to require the hand-written assembly language to be perfectly equivalent, but I wouldn't worry about it.

So the book you are quoting is technically incorrect. However, I would still write

execl(prog, arg, (char *) NULL);

if I was going to use NULL in the first place (I generally prefer to use 0 as the null pointer constant), because you shouldn't write code that relies on NULL expanding to ((void *)0), and

execl(prog, arg, 0);

is unquestionably incorrect. For example, execl will not receive a null pointer from that 0 on any ABI where int is 32 bits, char * is 64 bits, and int quantities are not sign- or zero-extended to 64 bits when passed as part of a variable argument list.


1 execl is not part of the C standard, but it is part of the POSIX standard, and any system providing execl in the first place is probably compliant with at least a subset of POSIX. All of clause 7.1.4 of the C standard can be assumed to apply to functions specified by POSIX as well.

Community
  • 1
  • 1
zwol
  • 135,547
  • 38
  • 252
  • 361
4

1) Does the C standard requires the null pointer be represented the same as (char*) 0

Yes, since a null pointer constant has type void *, and because a void * and a char * have the same representation.

This is detailed in section 6.3.2.3p3 of the C standard:

An integer constant expression with the value 0, or such an expression cast to type void * , is called a null pointer constant.

And section 6.2.5p28:

A pointer to void shall have the same representation and alignment requirements as a pointer to a character type. 48)

...

48) The same representation and alignment requirements are meant to imply interchangeability as arguments to functions, return values from functions, and members of unions.


2) Is (void *) 0 not a null pointer of a type? If yes, why can't the compiler cast (void *) 0 in execl(prog, arg, (void*) 0) to "a null pointer of the appropriate type"?

It is a null pointer of a type, and that type is void *.

The definition of execl is:

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

So it can't cast the third parameter to the appropriate type because it doesn't know what the appropriate type is, but it doesn't matter because void * and char * are interchangeable per 6.2.4p28 and footnote 48 as mentioned above.

3) Can the compiler cast (void *) 0 in execl(prog, arg, (void*) 0) to "a null pointer of the appropriate type" now? Why does it contradict to the quote in my point 2?

It still can't cast because it doesn't know what the appropriate type is. But again, it doesn't matter because void * and char * are interchangeable.

4) If I replace (void *) 0 in execl(prog, arg, (void*) 0) with cast of 0 to any type's pointer, such as (int *) 0, can the compiler cast (int *) 0 in execl(prog, arg, (int*) 0) to "a null pointer of the appropriate type"?

No, because again it doesn't know what the appropriate type is. And in this case you might have a problem if int * and char * don't have the same representation.

5) For a non-variadic function call, such as in sigaction(SIGINT, &sa, (int*) 0), can the compiler cast (int *) 0 to "a null pointer of the appropriate type"?

Yes, because (int *)0 is a null pointer, and because a null pointer can be converted to any other pointer.

dbush
  • 205,898
  • 23
  • 218
  • 273
  • "since a null pointer constant has type void *," - `0` is also a null pointer constant with type `int` – M.M Sep 06 '18 at 01:57
  • 2
    @EricPostpischil IMO C11 6.2.5p28 (with footnote 48) is trying to say that `char *` and `void *` can be used interchangeably in that situation . I cannot think of any other possible meaning or interpretation of that sentence. – M.M Sep 06 '18 at 02:02
  • @EricPostpischil personally I'll go with the intent expressed by 6.2.5p28 , after all the purpose of the standard is to codify an intended set of rules, and I think it is clear that they intended interchangeability here , even if the wording of 6.5.2.2/6 appears contradictory. – M.M Sep 06 '18 at 02:09
  • 1
    It wasn't there in C89, but in C99 and C11, the definition of `va_arg` contains a sentence specifically allowing `va_arg (ap, char *)` when the caller passed `void *`, and vice versa ([N1570: 7.16.1.1p2](http://port70.net/~nsz/c/c11/n1570.html#7.16.1.1p2), second bullet point). That's enough for me to endorse M.M's interpretation. I would argue, however, that one should write `(char *)0` or `(char *)NULL` in this context simply because one shouldn't ever rely on NULL having a particular definition. – zwol Sep 06 '18 at 02:26
  • *Yes, because `(int *)0` is a null pointer* Is it? Per [6.3.2.3, p3](https://port70.net/~nsz/c/c11/n1570.html#6.3.2.3p3): "An integer constant expression with the value 0, or such an expression cast to type void *, is called a null pointer constant. ..." `(int *)0` isn't `(void *)0`. – Andrew Henle Sep 06 '18 at 15:48
0

AFAI understand that default argument promotion is applied to var-args functions except for pointers, which are remained as is done (zwol's example also supports1). So when 0 is passed to kinda var-args functions e.g. exec() family, it is recognized as unadorned integer 0 in lieu of null-pointer. En passant, execl(prog, arg, 0); may work on systems where null-pointer's internal representation and integer 0's internal representation are same yet it's not obligatory.

execl(prog, arg, NULL); may also unintentionally work subject to either of the following

  • If NULL is defined on the system as integer 0, the aforementioned explanation is applied.
  • If NULL is defined on the system as null-pointer-constant as (void*)0, though in a variadic function the compiler can't cast (void*)0 to a null pointer of appropriate type, even since the C standards say char* and void* are required to have same internal representation.

For more, glance at here. Additional exaple from here.


1 For example, execl will not receive a null pointer from that 0 on any ABI where int is 32 bits, char * is 64 bits, and int quantities are not sign- or zero-extended to 64 bits when passed as part of a variable argument list.