5

If I defined a variadic function:

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

int f(char*s,...)
{ 
    va_list ap;
    int i=0;
    va_start(ap, s);
    while(s)
    {
       printf("%s ", s);
       i++;
       s=va_arg(ap,char*);
    }
    va_end(ap);
    return i;
}

int main()
{ 
    return f("a","b",0);
}

gcc (linux x64) compiles this and the exe runs and prints "a b ".

is there any need for a cast like:

return f("a","b",(char*)0)

on common systems?

effbiae
  • 1,087
  • 1
  • 7
  • 22
  • 5
    Considering `sizeof(0)` might be, and on a 64 bit system it is, less than `sizeof((char*)0)`. – DeiDei Apr 13 '18 at 10:04
  • 3
    Yes, because this is a varargs function the cast is needed: the caller needs to know which type/size of argument to pass ("push") to the function. (see, for instance `execl()` and `execlp()` ) – joop Apr 13 '18 at 10:15
  • @joop are varargs "push"ed on linux x64? – effbiae Apr 13 '18 at 10:27
  • I quoted the word "push", because C does not require a stack. x64 probably will use a stack, with 32 or 64 bits alignment requirements. (but this is all implementation-dependent) – joop Apr 13 '18 at 11:10
  • 1
    Variadic functions are horribly unsafe no matter what you do, so there's no such thing as good practice when using them. Good practice is to not use variadic functions in the first place. Their presence is a certain indication of bad program design. – Lundin Apr 13 '18 at 11:41
  • [How are variable arguments implemented in gcc?](https://stackoverflow.com/questions/12371450/how-are-variable-arguments-implemented-in-gcc) might answer the question. The accepted answer in particular is very good. – Lundin Apr 13 '18 at 11:49
  • @joop "Arguments to a variadic function are "promoted" to 64 bit values on linux x64 so there's no need to explicitly cast up to a 64bit value on this platform." – effbiae Apr 14 '18 at 06:45

2 Answers2

9

The compiler can't auto promote pointer for variadic parameter, for example that why when you want to print a pointer in printf you must cast it to void *:

printf("%p", (void *)ptr);

The same rule applies to all variadic functions, compiler can't know that your function expect a char *, and 0 is by default just an integer so yes you need to cast it (char *)0.


Even NULL must be cast with your function (char *)NULL


so why does main() work and when will it fail?

Your code don't really "work". Your code invoke undefined behavior, anything can happen, so no one can answer this. Here you are lucky, but next time maybe not.

P.P
  • 117,907
  • 20
  • 175
  • 238
Stargateur
  • 24,473
  • 8
  • 65
  • 91
  • so why does ```main()``` work and when will it fail? – effbiae Apr 13 '18 at 10:15
  • 1
    @effbiae it's "undefined behaviour" (google that). This includes "apparently working fine". – Jabberwocky Apr 13 '18 at 10:18
  • @MichaelWalz is it necessarily undefined on linux x64? – effbiae Apr 13 '18 at 10:26
  • 1
    @effbiae undefined behaviour is undefined. Depending on the implementation if may work all the time, it may work most of the time, it may never work, it may work all the time until you compile it with another compiler or until you upgrade your OS etc. – Jabberwocky Apr 13 '18 at 10:28
  • @MichaelWalz if the "common systems" were defined as x64 for Linux and Mac then the behaviour would be less undefined. For example, all values passed might be 8byte aligned and zero padded? – effbiae Apr 13 '18 at 10:31
  • 5
    @effbiae It's undefined behaviour. Don't rely on undefined behaviour. Period. There is no such thing as "less undefined behaviour" it is UB or it is not. – Jabberwocky Apr 13 '18 at 10:32
  • I highly doubt that NULL needs to be cast in this answer – effbiae Apr 13 '18 at 10:34
  • @MichaelWalz are you talking about a language spec? – effbiae Apr 13 '18 at 10:38
  • 2
    @effbiae You shouldn't rely on implementation specific detail with your code, only standard, specially in C. linux could change any when, without warm you because you wasn't suppose to rely on this behavior, it isn't user space. `NULL` must be cast because it's a `void *` and your function expect `char *`, like I said there is no auto promote, and standard is a little obscure about compatibility between `char *` and `void *`, in doubt always cast your variable with variable parameter function. – Stargateur Apr 13 '18 at 10:39
  • @Stargateur so much code relies on sizeof(void*)==sizeof(T*) - why would you cast NULL? Isn't NULL defined as a zero pointer in the lang spec? – effbiae Apr 13 '18 at 10:43
  • @effbiae Standard never guaranty that pointers are all the same size, it's implemented behavior. That is a choice, but I prefer avoid this kind of code when the "implemented behavior" can transform your code to "undefined behavior" and this is generally the case with pointer ;) – Stargateur Apr 13 '18 at 10:48
  • @effbiae You mix up thing, here I'm taking about `NULL`, `0` is an integer and with your code use zero is undefined behavior. I already say that in my answer. This comment section is too long so I end the conversation here. – Stargateur Apr 13 '18 at 10:52
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/168917/discussion-between-effbiae-and-stargateur). – effbiae Apr 13 '18 at 11:16
  • In this specific case `0` is a null pointer constant, and as such it can be implicitly converted to any object pointer just fine. Similarly, NULL is fine too without any cast, since there is already a manner of cast going on inside va_arg. See [How are variable arguments implemented in gcc?](https://stackoverflow.com/questions/12371450/how-are-variable-arguments-implemented-in-gcc). Problems do however arise if a null pointer constant has different size than a pointer. In such cases, a conversion is necessary. – Lundin Apr 13 '18 at 11:45
  • 3
    @Lundin "In this specific case", witch one ? this one ? `return f("a","b",0);` ? In this case is totally wrong. "it can be implicitly converted to any object pointer just fine", what do you mean ? that in the case case `0` is auto cast to `char *` ? This is totally wrong too. "Similarly, NULL is fine too without any cast, since there is already a manner of cast going on inside va_arg." => you answer yourself "Problems do however arise if a null pointer constant has different size than a pointer. In such cases, a conversion is necessary." so no NULL is NOT fine without a cast. – Stargateur Apr 13 '18 at 12:03
  • @MichaelWalz: There ARE different levels of undefined -- while this is undefined according to the C and C++ standards, it IS defined by the [SYSV x64 ABI standard](https://www.google.com/search?q=sysv+x64+abi+standard). – Chris Dodd Apr 14 '18 at 19:09
2

Your code should work fine on any system that supports the SYSV x64 ABI standard. It may work on systems that use some other ABI standard that has similar requirements, and may fail on any system that uses some other ABI1. This is all undefined according to the C standard.


1In particular, Microsoft does NOT follow the standard x64 ABI, they have their own standard.

Chris Dodd
  • 119,907
  • 13
  • 134
  • 226
  • because values are promoted to 64bit - or are 8 byte aligned? – effbiae Apr 15 '18 at 00:13
  • 1
    @effbiae: both -- all values in the x64 ABI standard are multiples of 8 bytes/64 bits and are passed as such. – Chris Dodd Apr 15 '18 at 01:48
  • Is x64 ABI standard define that NULL is zero ? If not your answer is wrong. I trough the question was "is there any need for a cast like: `return f("a","b",(char*)0)` on common systems?", for me common systems mean "in general", I'm very surprise the OP valid your answer, like if OP only want at any price valid that the code wasn't broken. That why Michael Walz and me didn't want to answer "why does main() work", we shouldn't encourage people that use undefined behavior. – Stargateur Apr 15 '18 at 08:40
  • @Stargateur doesn't the x64 ABI and the program that uses it demonstrate that the undefined behaviour you're talking about becomes defined on x64? "People" can be exposed to dangerous information like this and make their own choices. The question was not about what the C standard has to say. – effbiae Apr 15 '18 at 11:06
  • @Stargateur: yes, quoting the ABI standard "A null pointer (for all types) has the value zero". As I said in my comment to your answer, there ARE multiple kinds of defined/undefined and what is undefined in one standard may be defined in another. Code is only ever portable according to a standard -- if it follows the standard it will be portable to systems that follow that standard, but not to systems the don't. Knowing *which* standards the system(s) you are using is important. – Chris Dodd Apr 15 '18 at 18:11
  • This answer is not correct – the SysV x64 API does not actually specify this to work correctly. The upper 32 bits of the passed in `0` might contain garbage data. See my answer [here](https://stackoverflow.com/a/73888247/758345). – Chronial Sep 28 '22 at 22:03
  • @Chronial: Its not actually possible to write a 32-bit value to a 64-bit general purpose register in x86_64 *without* setting the upper 32 bits of the destination to 0. Every instruction that can take a 32 bit register as a destination is defined this way. So the only way to get garbage in the upper 32 bits of the register would be to store a 64 bit value there and not store a 32 bit value afterwards. The SYSV ABI does specify that the 32-bit argument passed in a register is placed in that register as a 32-bit value, so the upper 32 bits will always be 0. – Chris Dodd Sep 28 '22 at 23:31
  • The SysV ABI only specifies what the caller has to put into the lower part of the register, not how it has to get there. Writing `2^32+5` to a 64bit register to pass a `(int)5` argument seems ABI-compliant to me. But I agree that seems unlikely that a compiler would ever do something like that. – Chronial Sep 29 '22 at 00:30
  • The SysV ABI only specifies what the caller has to put into the lower part of the register, not how it has to get there. Writing 2^32+5 to a 64bit register to pass a (int)5 argument seems ABI-compliant to me. But I agree that seems unlikely that a compiler would ever do something like that. Reading your answer again more carefully, it is actually correct, since you said just that: “should work fine”. – Chronial Sep 29 '22 at 00:43