man stdarg shows, under the va_args macro description:

If ap is passed to a function that uses va_arg(ap,type), then the value of ap is undefined after the return of that function.

If we look at this following code:

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

void    print_next_arg(va_list ap)
{
    printf("%d\n", va_arg(ap, int));
}

void    testfunc(int n, ...)
{
    va_list ap;

    va_start(ap, n);
    print_next_arg(ap);
    print_next_arg(ap);
    va_end(ap);
}

int main(void)
{
    testfunc(2, 1, 2);
}

It is initializing a va_list in the testfunc function, sending it to another function which uses va_arg, before returning to the first function and continuing to use the va_list which, according to the man, should have an undefined value by that point.

Is this bad practice? If not, what did the man mean by that quoted line? If so, what would be good practice for utilizing va_arg in a sub-function, and why does my example code run perfectly fine on my machine?

14 Replies 14

By a pointer. print_next_arg(va_list *ap)

Is this bad practice?

Not really a practice. The presented code is just invalid.

what would be good practice for utilizing va_arg in a sub-function?

Passing va_list with a pointer.

This question could use some refinement. I don't understand why void print_next_arg(va_list *ap) isn't the answer.

Is a pointer all that's needed? Why does my code work anyway when I attempt to run it? And what does passing a pointer change since I'm passing ap as a direct value to va_arg anyway, and not by its address?

I'm not sure what your title has to do with the content of your question. The title asks "how can I pass a va_list to another function safely?", and in the question you instead ask about someone else's code (come on, cite them correctly!). "Safely" is a really contested term in C, because in all honesty, none of this is "safe", but that's inherent to the way things are here (some read-only argument being undefined after use is… hm. I'm having a hard time reconciling this with the concept of "safe").

See Passing variable number of arguments around

Make sure to also read the linked questions.

In my opinion, I prefer to do this:

void myvprintf(const char *fmt, va_list args) {
    // do something
}

void myprintf(const char *fmt, ...) {
    va_list args;

    va_start(args, fmt);
    printf(fmt, args);
    va_end(args);

    va_start(args, fmt);
    myvprintf(fmt, args);
    va_end(args);
}

And the reason is: if you can't guarantee 100% of times that the function call will create a copy of va_list variable, and call va_end afterwards on the copied object, you should at least avoid these potential errors from happening.

Besides, va_list is a black box to me. I don't want to mess with that.

The code will be larger? Yes. Potentially slower? Also yes. But it won't crash or give segmentation fault errors, and that's nice already.

"Why does my code work anyway when I attempt to run it?"

Because it happens to work for the implementation of C that you are using.

On some implementations, va_list is some sort of array type and on others it is some sort of structure type. Naturally, that will change the effect of using a va_list in a function call. If it is some sort of array type and the function uses va_arg, it may be modifying the caller's va_list. If it is some sort of structure type and the function uses va_arg, it will only modify a copy of the caller's va_list. Your example code might repeatedly print the same argument (2 2) rather than different arguments (2 1).

What @IanAbbott mentioned is spot on. One possible definition of va_list could look like this:

typedef struct __va_list_tag
{
    long __gpr;
    long __fpr;
    void *__overflow_arg_area;
    void *__reg_save_area;
} va_list[1];
//       ^^^

When you send in ap to print_next_arg it decays to a pointer to the first element in the array and

void    print_next_arg(va_list ap)

would be the same as writing

void    print_next_arg(__va_list_tag ap[1])
//or
void    print_next_arg(__va_list_tag *ap)

Now, if you call functions that changes what ap points at, like va_arg, those changes will be visible in the calling function too since that ap points at the same data.

If va_list is not defined to be of pointer type or of array type (that decays to a pointer when an object of such a type is sent in to a function), you would send in a copy of ap into print_next_arg and any changes to the copy would be invisible at the call site.

The portable way is therefore to send in a pointer to ap and dereference that pointer when using it with the va_* functions:

void print_next_arg(va_list* ap) {
//                         ^
    printf("%d\n", va_arg(*ap, int)); 
//                        ^
}

void testfunc(int n, ...) {
    va_list ap;
    va_start(ap, n);
    while (n-- > 0) {
        print_next_arg(&ap);
//                     ^
    }
    va_end(ap);
}

va_list and variadic functions are intrinsically dangerous and broken features of C, so any use of them is per definition not safe. Using them is per definition bad practice. As is using stdio.h in release builds. Just because something is common does not make it good.

All of this originates from a time of experimentation and confusion in the 1960s/1970s, when good programming practice had not yet been invented and programming languages just wanted to add a lot of features to make them look good. Nobody actually knew at the time which features that would make sense in the future.

The true mistake was done by C89 which let completely broken libraries like stdio.h enter the standard instead of coming up with something safe and useful. In 1989 they knew for sure that stdio.h came with a lot of dangerous trash functions and yet they decided to standardize it.

... 1960s/1970s, when good programming practice had not yet been invented and programming languages just wanted to add a lot of features to make them look good.

I'm so glad modern languages such as Java and C++ that stolidly base themselves on fundamentally-sound programming practices while eschewing flavor-of-the-moment, MY-NEW-SHINY! efforts to be everything for everyone would never do anything like that.

What the note about "ap being undefined on the return" from the first print_next_arg() is that the C compiler doesn't really know how many arguments from the va_list have been processed nor does it know their types so it doesn't know what the current va_arg() points to nor it's type or even if the current va_arg() item is a valid value.

Your example works because all of your arguments are int and the logic is the same as if all of the values were printed with a single printf() rather than several due to the simplicity of your example.

So your code is equivalent to using a single printf() as in

printf("%d\n%d\n", va_arg(ap, int), va_arg(ap, int));

The reason why your example works is because your example was written to work, whether knowingly or not. Try mixing up variable types and format types and see what you get.

C variable length arguments is a very primitive addition to the language. At least the stdarg.h functionality is better than what people used to do to implement variadic argument lists.

By the way there is a special version of the printf() function that takes a format and a va_list parameter, vprintf(). It works as long as the format describes the list of arguments in va_list so that vprintf() knows what type to use in the call to va_arg() as it prints each argument. However if the format provided doesn't match the actual argument list whether in the variable types or fewer arguments than the format specifies, you don't know what results you will get.

Thank you all for your answers. I'm still somewhat confused about how in my example ap isn't sent by pointer to va_arg() but needs to be sent by pointer to the function calling va_arg() or something very bad might happen. Maybe my confusion stems from me thinking va_arg() is a function while it's actually a macro, which is very different now that I think about it.

Richard, I also tried mixing types up between int and long but couldn't get any incorrect or unexpected return from va_arg(). I tried to mishandle it by giving it incorrect types but didn't manage to break it.

Thank you for mentioning that stdarg.h and stdio.h are bad practice. I still need to use those as part of my C programming course but at least now I know not to use them moving forward.

Right, so when a function instantiates a va_list and that list is consumed, then it becomes garbage, so to speak.

If you want to consume a va_list, yet have it be available again for another traversal, you can use the va_copy macro to create a copy.

va_copy was not present in ISO C90/ANSI C; it was introduced in C99.

As an alternative to va_copy, you can simply initialize a new list using va_start. But that can only be done in the original function where the parameter list is in scope. va_copy can be done anywhere; and it can create a "snapshot" of a partially consumed va_list; both the copy and the original then traverse the unconsumed remainder of the arguments.

"should have an undefined value" That is not what "is undefined" means. We happen to say "the value [of the variable] is undefined" but we MEAN "no value is accessible as stored in or denoted by the variable". "undefined value" is an oxymoron that arises from the way we use language. A value by definition cannot be undefined. A variable can have no value "defined" to be as stored in it. Similarly (C technical term) "undefined behaviour" isn't behaviour. The phrase is another oxymorong. Rather, "has undefined behaviour [of some code]" MEANS "has no behaviour defined". Understanding "undefined behaviour" is fundamental to understanding the meaning & behaviour of C programs.

Your Reply

By clicking “Post Your Reply”, 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.