10

When trying to compile this code

#include <stdarg.h>

void bar_ptr(int n, va_list *pvl) {
    // do va_arg stuff here
}

void bar(int n, va_list vl) {
    va_list *pvl = &vl; // error here
    bar_ptr(n, pvl);
}

void foo(int n, ...) {
    va_list vl;
    va_list *pvl = &vl; // fine here
    va_start(vl, n);
    bar(n, vl);
    va_end(vl);
}

int main() {
    foo(3, 1, 2, 3);
    return 0;
}

the GCC compiler prints a warning about initialization from incompatible pointer type in the bar function. The identical statement is fine in foo.

It seems that the type of an agument of type va_list is not a va_list. This can be tested easily with a static assertion like

_Static_assert(sizeof(vl) == sizeof(va_list), "invalid type");

in the bar function. With GCC, the _Static_assert fails. The same can be tested also in C++ with declytpe and std::is_same.

I would like to take the address of the va_list vl argument of bar, and pass it as argument of bar_ptr, to do thinks like the one described in this thread. On the other hand, it is fine to call bar_ptr(n, pvl) directly from main, replacing bar(n, vl).

According to the footnote 253 of the C11 final draft,

It is permitted to create a pointer to a va_list and pass that pointer to another function

Why this cannot be done if va_list is defined as argument of the function, and not in the function body?

Workaround:

Even if this does not answer the question, a possible workaround is to change the content of bar by using a local copy of the argument created with va_copy:

void bar(int n, va_list vl) {
    va_list vl_copy;
    va_copy(vl_copy, vl);
    va_list *pvl = &vl_copy; // now fine here
    bar_ptr(n, pvl);
    va_end(va_copy);
}
Giovanni Cerretani
  • 1,693
  • 1
  • 16
  • 30
  • 2
    Regarding the standards quote you have: You don't actually pass a pointer to a `va_list`. – Some programmer dude Feb 04 '20 at 11:00
  • 4
    *It is permitted to create a pointer to a va_list and pass that pointer to another function* You're not passing a **pointer** to a `va_list`, you're passing an actual `va_list`. – Andrew Henle Feb 04 '20 at 11:01
  • I know I'm passing a `va_list`. Suppose I have a third function `void bar_ptr(va_list *pvl);`, I want to pass a pointer to `va_list vl` to that function. I'll edit the question to specify it. – Giovanni Cerretani Feb 04 '20 at 11:02
  • If you call `bar_ptr` with `&vl` directly (as in `bar_ptr(n, &vl)`) do you get the same error then? – Some programmer dude Feb 04 '20 at 11:32
  • Also, are you on a 32 or 64 bit system? The `va_list` type can (and probably will) be different on an x86 system compared to an x86-64 system. – Some programmer dude Feb 04 '20 at 11:33
  • @Someprogrammerdude I get exactly the same error by calling `bar_ptr(n, &vl)`. I'm on a 64 bit system. I've noticed that it [works](https://godbolt.org/z/ieYk6d) when compiled with `-m32`. – Giovanni Cerretani Feb 04 '20 at 11:36
  • The problem is that, moving to C++ to use its tools, `std::is_same::value` is surprisingly false if used on `bar`. – Giovanni Cerretani Feb 04 '20 at 11:40
  • 1
    [This](https://stackoverflow.com/questions/12371450/how-are-variable-arguments-implemented-in-gcc) and [this](https://stackoverflow.com/questions/4958384/what-is-the-format-of-the-x86-64-va-list-structure) question could be helpful. And it's also very likely that the compiler have special handling of `va_list` – Some programmer dude Feb 04 '20 at 11:43
  • 1
    The `va_copy` approach you have is the canonical solution to this problem. I have a few past questions on it that basically concluded this. – R.. GitHub STOP HELPING ICE Feb 04 '20 at 13:48
  • @R..GitHubSTOPHELPINGICE an answer to my question is your answer [here](https://stackoverflow.com/a/4959184/3287591), even if that question was a little bit different. – Giovanni Cerretani Feb 04 '20 at 14:14
  • Another solution (C11+ only): `_Generic(vl, va_list: &vl, default: (va_list *)vl)` – R.. GitHub STOP HELPING ICE Feb 04 '20 at 14:19
  • For reference, the discussion about the [GCC bug #14557](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=14557) can be useful. – Giovanni Cerretani Feb 02 '22 at 11:17

2 Answers2

6

va_list is permitted by the standard to be an array, and often it is. That means va_list in a function parameter gets adjusted to a pointer to whatever va_list's internal first element is.

The weird rule (7.16p3) regarding how va_list gets passed basically accommodates the possibility that va_list might be of an array type or of a regular type.

I personally wrap va_list in a struct so I don't have to deal with this.

When you then pass pointers to such a struct va_list_wrapper, it's basically as if you passed pointers to va_list, and then footnote 253 applies which gives you the permission to have both a callee and a caller manipulate the same va_list via such a pointer.

(The same thing applies to jmp_buf and sigjmp_buf from setjmp.h. In general, this type of array to pointer adjustment is one of the reasons why array-typed typedefs are best avoided. It just creates confusion, IMO.)

Petr Skocik
  • 58,047
  • 6
  • 95
  • 142
  • 2
    If `va_list` may be an array, it is misleading for the standard to say “The object `ap` may be passed as an argument to another function…” (C 2018 7.16 3, `ap` is an object of type `va_list`). – Eric Postpischil Feb 04 '20 at 12:25
  • 3
    @EricPostpischil: The standard says a lot of misleading things and considers them not-a-but/WONTFIX as long as the intent is clear to "somebody"... :-P – R.. GitHub STOP HELPING ICE Feb 04 '20 at 13:49
  • Wouldn't it make more sense to just do as suggested by footnote 253 and pass a pointer to the `va_list` instead of the `va_list` itself? That will also work whether or not `va_list` is an array type, and avoids copying a possibly bulky object. (Sorry to comment after so much time. Your answer came up during a search for problems caused by `va_list` sometimes being an array type.) – rici Apr 11 '22 at 22:26
  • @rici Wrapping a might-be-an-array type in a struct gives it more predictable semantics from the user's point of view: passing an address to it gives you reference semantics, passing *it* gives you value semantics and you don't get the highly unintuitive thing where `might_be_an_array_t block_scope_var;` is typed differently from `might_be_an_array_t arg_variable;`. Unfortunately for a `struct wrapped_va_list`, the standard doesn't really guarantee that copies of that will work as expected as the standard wants you to do va_copy+va_end to get a copy of the current state. – Petr Skocik Apr 12 '22 at 07:05
  • @rici And yes, you usally want reference semantics anyway and it doesn't matter if you get that by passing `struct wrapped_va_list*` or always passing `va_list*` (might as well always pass va_list, because you never get value semantics with it anyway--you either get reference semantics if the caller passing a va_list doesn't use the passed `ap` after the call (other than doing `va_end(ap)` on it) or you get undefined behavior if it tries to use it after passing it to a callee). – Petr Skocik Apr 12 '22 at 07:06
  • @ric But for the rare case where you do want a copy (in a callee, which might then use subcallees to process parts) passing a `struct wrapped_va_list` might likely get you that without the ensuing weirdness stemming from `va_list param` not really being typed `va_list`. (And it doesn't preclude being proper and using `va_copy`/`va_end` on the member) – Petr Skocik Apr 12 '22 at 07:09
  • @PSkocik: Suppose that bit-for-bit copying of a `va_list` doesn't work (perhaps because registers are saved into the `va_list` itself, which also contains an internal pointer to the first unused one). Passing the `va_list` by value violates the "no direct copy" requirement, and you no longer have the original in order to perform the legitimate `va_copy`. So I think it does "preclude being proper". (Of course, it's dancing angels, unless there is actually an implementation which works like my imaginary one.) – rici Apr 12 '22 at 15:24
  • @rici What I meant was that merely having a wrapper type doesn't preclude doing `va_copy(dest.member,src.member)` and then passing `&dest`. – Petr Skocik Apr 12 '22 at 15:57
3

Another solution (C11+ only):

_Generic(vl, va_list: &vl, default: (va_list *)vl)

Explanation: if vl has type va_list, then va_list isn't an array type and just taking the address is fine to get a va_list * pointing to it. Otherwise, it must have array type, and then you're permitted to cast a pointer to the first element of the array (whatever type that is) to a pointer to the array.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711