7

I have a function foo(char *n, ...); I need to get and use all of optional char parameters. I had an idea to use

while(va_arg(argPtr, char) != NULL)
{
   ...
}

to understand when I reach the end of the list. So, will it work, if in function call I'll do foo(n, 't', 'm', '$', NULL); ?

Will NULL be read as a char by va_arg? Or maybe there's a more common way to determine the end of list, without adding NULL as last parameter?

kekyc
  • 317
  • 6
  • 15
  • if `n` is a format string, then you'd normally just behave like `printf` and family by matching the number of format specifiers against the number of parameters. Are you sure it's just a variable number of `char`s being passed? If so you can match against `-1` or `0` or something, but NULL is a bad idea for the reasons outlined by @hvd in his comments below. – Ryan Haining May 20 '16 at 21:35
  • @kekyc if you are relying on negative `char` values then you should be using `signed char`, if you're only dealing with values in the [0, 128) range (ascii) then -1 should be fine. – Ryan Haining May 20 '16 at 21:45
  • 2
    If you're passing a variable-length list of `char` arguments, why not just pass a string? (There are *some* cases where that wouldn't work, but it's the obvious solution.) – Keith Thompson May 20 '16 at 21:45
  • If you have a variable number of parameters of the same type, terminated by a special value, then that value needs to match the type of arguments. If you're passing `int` values, then you need to pick some special `int` value such as 0 or -1. If you're passing strings, i.e. `char *`, then you need to terminate it with `(char *) NULL`, as for example `execl()` requires. – Tom Karzes May 20 '16 at 21:46
  • Ok, thank you, guys, for awesome, full answers! – kekyc May 20 '16 at 22:03

3 Answers3

14

There is no direct way for a function that uses va_arg to determine the number or type(s) of the arguments passed by a given call.

Your proposed method in particular:

while(va_arg(argPtr, char) != NULL)

is incorrect. va_arg(argPtr, char) yields a value of type char, while NULL is a null pointer constant. (NULL is commonly defined as 0, which compares equal to the null character '\0', but you can't rely on that.)

Any variadic function must have a way for the caller to specify the number and types of arguments. The *printf functions, for example, do so via the (non-variadic) format string. The POSIX execl*() functions expect a sequence of char* arguments; the end of the argument list is marked by the caller with (char*)NULL. Other methods are possible, but they almost all depend on information given at run time in the arguments. (You could use some other method, such as a global variable. Please don't.)

This places a burden on the caller to ensure that the arguments passed to the function are consistent. The function itself has no way to confirm this. Incorrect calls, like printf("%d\n", "hello") or execlp("name", "arg1") have undefined behavior.

One more thing: you can't use va_arg with an argument of type char. When you call a variadic function, arguments corresponding to the , ... are promoted. Integer arguments of types narrower than int are promoted to int or to unsigned int, and arguments of type float are promoted to double. If a caller passes an argument of type char, the function must invoke va_arg(argPtr, int).

(In very obscure circumstances that you're not likely to run into, char can be promoted to unsigned int. That can happen only if plain char is unsigned and sizeof (int) == 1, which implies that a byte is at least 16 bits.)

Keith Thompson
  • 254,901
  • 44
  • 429
  • 631
  • 2
    Very nice and complete. Obscure methods include a global variable or _something_ amongst the arguments (not necessarily a ending sentinel). Yet the 2 ways mentioned by this fine answer are the most common. – chux - Reinstate Monica May 20 '16 at 22:27
  • @chux: Good point about global variables. I've added appropriate weasel words. – Keith Thompson May 20 '16 at 22:29
  • I recall these weak coding practices from the days when `foo(...)` was allowed - pre C89. – chux - Reinstate Monica May 20 '16 at 22:38
  • @chux: Interesting. I didn't know that pre-ANSI variadic functions (presumably using `` to fetch their arguments) used the `...` syntax. – Keith Thompson May 20 '16 at 22:44
  • Even some C99 compilers allowed [zero](https://en.wikipedia.org/wiki/Variadic_macro#Declaration_syntax) args as an extension. If the syntax was `...` or not, I may have remembered incorrectly - but the point is that _zero_ args _were_ coded at some point in the passed as an extension and/or pre-C89. It was rarely useful. – chux - Reinstate Monica May 21 '16 at 01:57
  • Isn't `NULL` explicitly interpreted as `((void* )0x0)`. Assigning it to an integer type should produce a warning. – Edenia Mar 30 '19 at 23:58
  • 1
    @Edenia: In C, `NULL` expands to "an implementation-defined null pointer constant". A null pointer constant is defined as "An integer constant expression with the value 0, or such an expression cast to type`void *`". So `NULL` could expand to any of `((void*)0)`, or `0`, or `('-'-'-')` if the implementer is in a perverse mood. Whether `int n = NULL;` will produce a diagnostic depends on the implementation. (Note that C++ has different rules.) – Keith Thompson Mar 31 '19 at 04:01
4

Just a few years late at this point, but I have some interesting contributions. In this case, I would use the pre-processor. Be warned of incorrect types with variadics btw.

Note: Parenthesizing the function name will make macros ignore it while allowing the file symbols to not be different than the API (other than macro usage).

Method 1: Sentinel Macro

Mask your functions as so:

#define my_func(...) my_func(__VA_ARGS__, NULL);
void (my_func)(...)
{
   /* ... */
}

Method 2: Array Length Macro

Cast the variadic to a int[], to statically determine sizeof said array:

#define ARRAY_LENGTH(a) (sizeof(a) / sizeof((a)[0]))              
#define NARGS(...) ARRAY_LENGTH((int[])({ __VA_ARGS__ }))

Similarly mask, but add a count argument in front:

#define my_func(...) my_func(NARGS(__VA_ARGS__), __VA_ARGS__);
void (my_func)(int n, ...)
{
   /* ... */
}

Compile-Specific Type Checking For the Array Macro:

Outside the scope of the question, but since I'm on a roll.

I always use GCC, and sometimes utilize the following. I believe there are semi-compatible features in Clang, but I'm not sure. This allows you to be sure an actual array is being passed and not a pointer.

#define TYPE_COMPATABLE(x, y) __builtin_types_compatible_p(__typeof__(x), __typeof__(y))
#define CHOOSE_EXPR(b, x, y) __builtin_choose_expr((b), (x), (y))
#define IS_ARRAY(a) (!TYPE_COMPATABLE((a), &(a)[0]))
#define ARRAY_LENGTH(a)                           \
    ({                                            \
        _Static_assert(IS_ARRAY(a), "non-array"); \
        sizeof(a) / sizeof((a)[0]);               \
    })

#define NARGS(...) ARRAY_LENGTH((int[])({ __VA_ARGS__ }))

At that point, might just use C++ hehe.

Hunter Kohler
  • 1,885
  • 1
  • 18
  • 23
1

The basic idea would work. But you've filled in the details in a way that almost certainly won't.

The usual implicit conversion rules don't apply when you're using variadic arguments. Instead, default argument promotions take place, which means that any integer type smaller than int/unsigned int gets converted to one of those -- that's not the only promotion, but the only one relevant here -- and which also means that there is no automatic conversion to whatever type you specify with va_arg.

This means that:

  • You cannot ever use va_arg(..., char), since it's impossible to pass a char as a variadic function argument.
  • You cannot ever pass NULL as a variadic function argument, since its concrete type is heavily implementation-dependent. Realistic types are int, long, void *, and there are loads of other less realistic but equally valid types.

You could change

while(va_arg(argPtr, char) != NULL)

to

while(va_arg(argPtr, int) != 0)

and the call

foo(n, 't', 'm', '$', NULL);

to

foo(n, 't', 'm', '$', 0);