tags:

views:

123

answers:

4

With the following code va_arg is returning garbage for the second and third pass through vProcessType.

// va_list_test.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <tchar.h>
#include <cstdarg>
#include <windows.h>

void processList(LPTSTR str, ...);
void vProcessList(LPTSTR str, va_list args);
void vProcessType(va_list args, int type);

int _tmain(int argc, _TCHAR* argv[])
{
    LPTSTR a = TEXT("foobar");
    int b = 1234;
    LPTSTR c = TEXT("hello world");
    processList(TEXT("foobar"), a, b, c);
    return 0;
}

void processList(LPTSTR str, ...)
{
    va_list args;
    va_start(args, str);
    vProcessList(str, args);
    va_end(args);
}
void vProcessList(LPTSTR str, va_list args)
{
    vProcessType(args, 1);
    vProcessType(args, 2);
    vProcessType(args, 1);
}

void vProcessType(va_list args, int type)
{
    switch(type)
    {
    case 1:
        {
            LPTSTR str = va_arg(args, LPTSTR);
            printf("%s", str);
        }
        break;
    case 2:
        {
            int num = va_arg(args, int);
            printf("%d", num);
        }
        break;
    default:
        break;
    }
}

Is passing a va_list thing not allowed in this way? The first call to va_arg inside vProcessType returns the expected string. The second and third time through this function it returns a pointer to the start of the first string, instead of the values expected.

If I hoist the va_arg call to vProcessList, everything seems to work fine. It only seems when I pass a va_list through a function that I'm getting this behaviour.

+4  A: 

You're passing the same va_list each time to vProcessType() - in each call to vProcessType() you're acting on the first va_arg in the list.

So you're always dealing with the TEXT("foobar") parameter when calling vProcessType().

Also note that the standard has this to say about passing a va_list to another function:

The object ap [of type va_list] may be passed as an argument to another function; if that function invokes the va_arg macro with parameter ap, the value of ap in the calling function is indeterminate and shall be passed to the va_end macro prior to any further reference to ap.

A foot note in the standard indicate that it's perfect OK to pass a pointer to a va_list, so what you might want to do is have vProcessType() take a pointer to the va_list:

void vProcessType(va_list* pargs, int type);
Michael Burr
... and this is undefined behavior. The only legal way to make a copy if a `va_list` object is a `va_copy` macro. I.e. it is not exactly correct to say that "you're acting on the first `va_arg` in the list". The behavior is simply undefined.
AndreyT
/facepalmThanks, I've modified vProcessType to vProcessType(va_list* pArgs, int type) instead, and added a "*pArgs = args" at the end of vProcessType.Seems to have worked. Thanks for your help.
Jonathan Yee
@AudreyT Windows doesn't seem to have a va_copy macro. What's the recommended solution here?
Jonathan Yee
@Jonathan Yee: It depends on your intent (read my answer). In short: if you want *continuous* parsing, you have to pass the `va_list` object *by pointer*, as others already suggested. If you want to start from the same point each time, you have to use `va_copy`, which is C99 macro (not present in C89/90, which is why you don't have it in your compiler).
AndreyT
@Jonathan Yee: From your example it appears that you need *continuous* parsing, meaning that there's no need for `va_copy` in your case. Simply pass the `va_list` object by pointer.
AndreyT
+3  A: 

You're passing your va_list by value. Try passing a pointer to the one value instead (or if you wanted a C++ only version, you could use a reference):

void vProcessList(LPTSTR str, va_list args)
{
    vProcessType(&args, 1);
    vProcessType(&args, 2);
    vProcessType(&args, 1);
}

void vProcessType(va_list *args, int type)
{
    ...
    LPTSTR str = va_arg(*args, LPTSTR);
    ...
    int num = va_arg(*args, int);
}
R Samuel Klatchko
+2  A: 

When you pass the va_list object to another function and that another function uses va_arg on the corresponding parameter, that va_list will have indeterminate value in the calling function when the control returns. The only thing you are allowed to do is to apply va_end to that va_list object.

This is how it is stated in the standard (7.15/3)

If access to the varying arguments is desired, the called function shall declare an object (generally referred to as ap in this subclause) having type va_list. The object ap may be passed as an argument to another function; if that function invokes the va_arg macro with parameter ap, the value of ap in the calling function is indeterminate and shall be passed to the va_end macro prior to any further reference to ap.

Don't pass va_listobjects by value. If your intent is to continue argument parsing in each consequent sub-function, then you have to pass the va_list object by pointer.

If you really want to pass your va_list object by value, i.e. if you want each sub-function to parse from the same point, you have to manually copy your va_list object in advance by using the va_copy macro. ("In advance" means that you have to make as many copies as you'll need before any sub-function has a chance to do a va_arg on it.)

AndreyT
+1  A: 

As others have noted, the C99 standard allows a portable (among C99 implementations) way for a program to run through a va_list more than once. There isn't any nice portable way to do that in pre-C99 implementations. What I did when I needed to run through a printf list more than once (for a "center string" function which had to evaluate the arguments once to determine how wide they would be, and then a second time to actually display them) was to examine the compiler vendor's "stdarg.h" and fudge my own implementation of the necessary functionality.

If one wanted a really portable implementation that would work on earlier C compilers, and if one knew in advance the maximum number of passes that would be required, I think one could create an array of va_ptr objects, use va_start on all of them, and then pass the array to the child routine. Sorta icky, though.

supercat
There's a very easy way to portably run through the arguments more than once - just use va_start more than once. The only time you need va_copy is to peek ahead in the middle of a run through the arguments without trashing the state, or if you're passing a va_list between functions and the called function needs to avoid ruining the caller's copy.
R..
That's nice if all the multi-pass code can go in the outer-level function. In my case, I put my own wrappers around stdarg stuff to allow for doing a va_copy of a partially-scanned argument list (not sure if the real va_copy would allow that), to allow for displaying a return-delimited list of lines (e.g. display two lines with show_and_wait2("Time=\205:\242\rDistance=\205", minutes,seconds, distance);). That would likely require four or more passes through the list, and I didn't want to duplicate all the multi-pass logic in show_and_wait2(), show_and_wait3(), etc.
supercat