Reputation: 138211
I wrote a function that accepts a va_list
, and that is meant to be invoked iteratively by its caller. It should modify the va_list
and changes should persist back in the caller so that the next call to the function will proceed with the next argument.
I can't post that code specifically, but here's a snippet that reproduces the situation (godbolt link):
#include <stdarg.h>
#include <stdio.h>
void print_integer(va_list ap) {
printf("%i\n", va_arg(ap, int));
}
void vprint_integers(int count, va_list ap) {
for (int i = 0; i < count; ++i) {
print_integer(ap);
}
}
void print_integers(int count, ...) {
va_list ap;
va_start(ap, count);
vprint_integers(count, ap);
va_end(ap);
}
int main() {
print_integers(3, 1, 2, 3);
}
This works (prints "1 2 3") on my x86 platform because va_list
is passed by "reference" (it's probably declared as an array of one element, so va_list
arguments decay to a pointer). However, it does not work on my ARM platform (prints "1 1 1"), where va_list
seems to be defined as a pointer to something. On that platform, va_arg
always returns the first argument.
The next best option seems to be to make ap
a pointer:
#include <stdarg.h>
#include <stdio.h>
void print_integer(va_list *ap) {
printf("%i\n", va_arg(*ap, int));
}
void vprint_integers(int count, va_list ap) {
for (int i = 0; i < count; ++i) {
print_integer(&ap);
}
}
void print_integers(int count, ...) {
va_list ap;
va_start(ap, count);
vprint_integers(count, ap);
va_end(ap);
}
int main() {
print_integers(3, 1, 2, 3);
}
This works on ARM (with va_arg(*ap, ...)
), but it does not compile on x86. When I try print_integer(&ap)
on x86, Clang says:
error: incompatible pointer types passing '
struct __va_list_tag **
' to parameter of type 'va_list *
' (aka '__builtin_va_list *
')
This only seems to happen when taking the address of a va_list
passed as an argument, not when it's taken from a local variable. Unfortunately, I do need my v
variant to take a va_list
object and not a pointer to it.
It's easy to get consistent cross-platform value semantics for va_list
using va_copy
. Is there a cross-platform way to get consistent reference semantics for va_list
?
Upvotes: 1
Views: 768
Reputation: 369
Here's a trick that I believe works in all versions of ANSI C: wrap the va_list in a structure, and pass around pointers to that.
#include <stdarg.h>
#include <stdio.h>
struct va_list_wrapper {
va_list ap;
};
void print_integer(struct va_list_wrapper *wap) {
printf("%i\n", va_arg(wap->ap, int));
}
void vprint_integers(int count, struct va_list_wrapper *wap) {
for (int i = 0; i < count; ++i) {
print_integer(wap);
}
}
void print_integers(int count, ...) {
struct va_list_wrapper wap;
va_start(wap.ap, count);
vprint_integers(count, &wap);
va_end(wap.ap);
}
int main() {
print_integers(3, 1, 2, 3);
}
Upvotes: 0
Reputation: 138211
The thing at issue here is that va_list
, on the x86 platform, is defined as an array of 1 element (let's call it __va_list_tag[1]
). It decays to a pointer when accepted as an argument, so &ap
is wildly different depending on whether ap
is a parameter of the function (__va_list_tag**
) or a local variable (__va_list_tag(*)[1]
).
One solution that works for this case is simply to create a local va_list
, use va_copy
to populate it, and pass a pointer to this local va_list
. (godbolt)
void vprint_integers(int count, va_list ap) {
va_list local;
va_copy(local, ap);
for (int i = 0; i < count; ++i) {
print_integer(&local);
}
va_end(local);
}
In my case, vprint_integers
and va_copy
are necessary because the interface of vprint_integers
accepts a va_list
and that cannot change. With more flexible requirements, changing vprint_integers
to accept a va_list
pointer is fine too.
va_list
isn't specified to be anything in particular, but it's defined to be an object type, so there's not really a reason to believe that you can't take or pass its address. Another very similar solution that entirely bypasses the question of whether you can take the address of a va_list
is to wrap the va_list
in a struct and pass a pointer to that struct.
Upvotes: 2
Reputation: 141638
The problem in the second program is that if va_list
is an array type, then the function parameter ap
has its type adjusted to be a pointer type. So the argument has a different level of indirection in each case.
Since C11 we can solve this with a generic selector to test whether or not ap
is still a va_list
:
void vprint_integers(int count, va_list ap) {
for (int i = 0; i < count; ++i) {
print_integer((va_list *)_Generic(&ap, va_list*: &ap, default: ap));
}
This solution was inspired by this answer which used a macro for the two cases that required manual configuration .
Notes about the _Generic
:
&ap
because, since C18, array-to-pointer decay is performed on the first expression._Generic(&ap, va_list*: &ap, default: (va_list *)ap)
however this is rejected by compilers which check for constraint violations in all branches for all inputs -- although do not go on to check constraint violations in the surrounding expressions for unselected branches.Upvotes: 2
Reputation: 165396
You may be out of luck. 7.15(3) says what you're doing has indeterminate effects.
The object
ap
may be passed as an argument to another function; if that function invokes theva_arg
macro with parameterap
, the value ofap
in the calling function is indeterminate and shall be passed to theva_end
macro prior to any further reference toap
.
The call to va_arg
in print_integer
is causing ap
in print_integers
to be indeterminate. On one architecture it's being incremented, on another it isn't.
As for what va_list
is, it could be anything...
...which is an object type suitable for holding information needed by the macros va_start, va_arg, va_end, and va_copy.
You may wish to consider a different approach to solving the underlying problem.
Upvotes: -1