通过引用传递va_list的跨平台方法是什么?

时间:2020-04-01 20:28:19

标签: c

我编写了一个接受va_list的函数,该函数打算由其调用方迭代地调用。它应该修改va_list,并且更改应继续存在于调用方中,以便对函数的下一次调用将继续使用下一个参数。

我无法具体发布该代码,但这是一个重现此情况的代码段(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);
}

这在我的x86平台上有效(打印“ 1 2 3”),因为va_list是通过“引用”传递的(它可能被声明为一个元素的数组,因此va_list自变量衰减为a指针)。但是,它在我的ARM平台上(打印“ 1 1 1”)起作用,其中va_list似乎被定义为指向某物的指针。在该平台上,va_arg始终返回第一个参数。

下一个最佳选择似乎是使ap成为指针:

#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);
}

这在ARM(带有va_arg(*ap, ...))上有效,但是不能在x86上编译。当我在x86上尝试print_integer(&ap)时,Clang说:

错误:不兼容的指针类型将“ struct __va_list_tag **”传递给类型“ va_list *”(又名“ __builtin_va_list *”)的参数

这似乎仅在将va_list的地址作为参数传递时发生,而不是从局部变量中获取时发生。不幸的是,我确实需要我的v变体来获取va_list对象,而不是指向它的指针。

使用va_list可以很容易地为va_copy获得一致的跨平台值语义。是否有一种跨平台的方法来获取va_list的一致引用语义?

3 个答案:

答案 0 :(得分:1)

这里要讨论的问题是,在x86平台上,va_list被定义为1个元素的数组(我们称之为__va_list_tag[1])。当作为参数接受时,它会衰减为指针,因此&ap取决于ap是函数(__va_list_tag**)的参数还是局部变量({{1} }。

一种适用于这种情况的解决方案是简单地创建一个本地__va_list_tag(*)[1],使用va_list进行填充,然后将指针传递给该本地va_copy。 (godbolt

va_list

在我的情况下,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); } vprint_integers是必需的,因为va_copy的接口接受vprint_integers,并且不能更改。有了更灵活的要求,将va_list更改为接受vprint_integers指针也是可以的。

va_list并没有特别指定任何东西,但是它被定义为对象类型,因此没有真正的理由相信您不能接受或传递其地址。另一个非常相似的解决方案完全绕开了是否可以使用va_list的地址的问题,是将va_list包装在一个结构中并传递一个指向该结构的指针。

答案 1 :(得分:0)

您可能不走运。 7.15(3)表示您的操作具有不确定的效果。

对象ap可以作为参数传递给另一个函数。如果该函数使用参数va_arg调用ap宏,则调用函数中ap的值是不确定的,应在进一步引用之前传递给va_end宏到ap

va_arg中对print_integer的调用导致ap中的print_integers不确定。在一种体系结构中,它正在增加,而在另一种体系结构中,则没有。

关于va_list是什么,它可以是任何东西...

...是适合于保存宏va_start,va_arg,va_end和va_copy所需信息的对象类型。

You may wish to consider a different approach to solving the underlying problem

答案 2 :(得分:0)

第二个程序中的问题是,如果va_list是数组类型,那么函数参数ap的类型 adjusted 就是指针类型。因此,在每种情况下,该参数都有不同的间接级别。

自C11以来,我们可以使用通用选择器解决此问题,以测试ap是否仍然是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 answer,它在需要手动配置的两种情况下都使用了宏。

关于_Generic的注释:

  • 我们必须测试&ap,因为自C18以来,对第一个表达式执行了数组到指针的衰减。
  • 最初我有_Generic(&ap, va_list*: &ap, default: (va_list *)ap),但是编译器拒绝了这一点,编译器会检查所有输入中所有分支的约束违规情况-尽管不会继续检查周围表达式中未选择分支的约束违规情况。