duper21
duper21

Reputation: 429

How does the following va_arg macro work?

I am reading a stdarg.h (link below) header file which defines the va_arg macro as follows

/*
 * Increment ap to the next argument in the list while returing a
 * pointer to what ap pointed to first, which is of type t.
 *
 * We cast to void* and then to t* because this avoids a warning about
 * increasing the alignment requirement.
 */
#define va_arg(ap, t)                   \
 (((ap) = (ap) + __va_argsiz(t)),       \
  *((t*) (void*) ((ap) - __va_argsiz(t))))

The line

((ap) = (ap) + __va_argsiz(t))

reassigns the value of ap however I don't understand the purpose of the comma or the line

*((t*) (void*) ((ap) - __va_argsiz(t)))

Link to stdarg.h file

Upvotes: 4

Views: 172

Answers (3)

Steve Summit
Steve Summit

Reputation: 47923

Suppose I have a char * pointer p that points to some characters. Suppose I want to return the character pointed to by p, and simultaneously increment p to point to the next character. That's easy, that's just *p++ — a totally basic operation in C.

And because of the way pointer arithmetic works, if I have an int * pointer ip that points to some integers, I can do exactly the same thing. *ip++ returns the integer pointed to by ip, and it increments ip to point to the next integer. Crucially, if we were to look at the actual value of the pointer ip both before and after this operation, we would find that it had been incremented by sizeof(int), not just by 1.

Now, va_arg(ap, t) is sort of like *ip++. It returns the argument "pointed to" by ap, and it increments ap to point to the next thing in the list. But, and this is a very big "but", we don't necessarily want to increment by sizeof(int). We want to increment by sizeof(t), where t is whatever the caller told us is the currently-expected argument type in the list. (We can assume that the actual type of the pointer ap is char *, so that it increments by individual bytes, so that it's meaningful to add sizeof(t) to it.)

So what we, sort of, want is the effect of *(ap += sizeof(t)). But += gives you preincrement, and we want postincrement. And while C gives you the handy distinction between preincrement ++ and postincement ++, the += operator is always preincrement. There's no postincrement form.

So the code you've shown for va_arg has to simulate a postincrement += the hard way, using two separate expressions separated by a comma operator. First it adds sizeof(int) to ap using the subexpression (ap) = (ap) + __va_argsiz(t), and then it returns what ap used to point to by subtracting the size it just added: *((t*) (void*) ((ap) - __va_argsiz(t))).

The comma operator always does two things in sequence: it does the first thing and throws away the result, then it does the second thing and returns the second thing's value. This definition of v_arg has been arranged to take advantage of that definition. We don't care about the value of the first subexpression — the point of the first expression is its side effect, of adding something to ap. What we need the value of (that is, to be va_arg's value) is the second subexpression.

(In other words, based on the way the comma operator works, it's not possible to write an expression that first returns what ap points to, then adds something to ap. So the author of this va_arg macro had no choice but to do the addition a bit too early, meaning that it had to be counteracted by the subtraction in the second half.)

There's some other magic going on in this definition of va_arg — which is hardly surprising, as it's a pretty magical macro! — but this should at least explain what the comma operator is doing there.

Upvotes: 1

user58697
user58697

Reputation: 7923

We need to return to the caller what ap has been pointed to, and advance ap. An equivalent would be

    old_ap = ap
    ap = ap + argsiz
    return *old_ap

However, that would require an extra variable, which is very hard (if possible) deal with in a macro in a portable way. Instead, the macro relies on a comma expression. It advances ap, and then computes its old value, which becomes a value of the comma expression, that is of the entire macro.

Upvotes: 2

Vlad from Moscow
Vlad from Moscow

Reputation: 310950

There is an expression with the comma operator.

The first sub-expression

(((ap) = (ap) + __va_argsiz(t))

increase the pointer ap.

The second sub-expression

*((t*) (void*) ((ap) - __va_argsiz(t))))

returns the value pointed to by the pointer ap before its increment.

If you have a question relative to the comma operator then it allows to combine several full expressions in one expression.

Consider for example the following program

#include <stdio.h>

int main(void) 
{
    int a[] = { 1, 2 };

    size_t i = 0;

    printf( "%d\n", ( i++, a[i] ) );

    return 0;
}

Its output is

2

Here the argument of the printf call is an expression with the comma operator.

Or a more interesting example

#include <stdio.h>

int main(void) 
{
    int a[] = { 1, 2 };

    size_t i = 0;

    printf( "%d\n", a[i] ), i++, printf( "%d\n", a[i] );

    return 0;
}

The program output is

1
2

Here the expression statement

    printf( "%d\n", a[i] ), i++, printf( "%d\n", a[i] );

contains an expression with using two comma operators.

Upvotes: 2

Related Questions