变长函数传递 long 但读取为 va_arg(argList, int)

Variadic function passing long but reading as va_arg(argList, int)

我正在将 32 位应用程序转换为 64 位应用程序,我遇到的痛点之一是 运行 可变参数函数,它期望很长但可能会传递一个整数,例如参数被硬编码为 -1 而不是 -1L,这是因为 long 大小的 64 位更改为 64 位。拿这个例子代码:

#include <stdio.h>
#include <stdarg.h>

long varargsExample(int input, ...);

int main(int argc, char **argv)
{
    varargsExample(5,
    "TestInt", 0,
    /* This will fail if read as a long */
    "TestIntNegative", -1,
    "TestLong", 0L,
    "TestLongNegative", -1L,
    NULL); 
}

long varargsExample(int firstArg, ...)
{
    va_list args;
    char * name;
    long nextValue;

    va_start(args, firstArg);
    while ((name = va_arg(args, char *)) != 0)
    {
        /* If the type is changed to read in an int instead of long this works */
        nextValue = va_arg(args, long);

        printf("Got [%s] with value [%ld]\n", name, nextValue);

    }
    va_end(args);
    return 0;

}

运行 使用 GCC 64 位编译时,结果为:

Got [TestInt] with value [0]
Got [TestIntNegative] with value [4294967295]
Got [TestLong] with value [0]
Got [TestLongNegative] with value [-1]

这是有道理的,因为我猜这会被解释为:

0000 0000 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 1111 1111 1111 1111

所以填充额外的 32 位来表示长,我们得到 2^32 - 1 而不是负数。然而,我想知道的是,如果我将 va_arg 读取更改为将值读取为 int,则无论传递的是 int 还是 long,这似乎都有效,例如:

nextValue = va_arg(args, int);

这是一个恰好有效的 hack,还是 C 规范中的某些内容使其能够始终如一地工作?请注意,此应用程序在 Unix/Linux 和 Windows 上运行,在 Windows 上运行 long 是 32 位,所以我不担心函数传递的值不能由32 位整数。我创建了一个基本的单元测试,它通过 INT_MIN --> INT_MAX 传递给可变参数函数 integers/longs 的混合并读取为 va_arg(args, int) 并且它似乎可以工作(在 AIX、Solaris 和 RHEL 上测试过),但我不确定这是否只是碰巧在这些平台上工作的未定义行为。

此处的正确解决方法是识别此函数的所有调用者并确保它们在所有情况下都传递 long 但这些函数的使用相当 widespread/hard 以在没有编译器支持识别的情况下进行识别这个。如果有 GCC 扩展我可以利用它来指定自定义可变参数类型检查,我试图将其视为替代方案,类似于格式参数检查(sprintf、printf 等)所做的。

编译器对参数执行 not know which types the variadic function fetches from the list, so it as to rely on the types of arguments given. It performs the default argument promotions

对于整数类型,它们基本上将 "smaller" 类型提升为 intunsigned,并传递 int/unsigned 和 "larger"类型不变。

获取参数时,your responsibility从可变参数中获取正确的类型。任何其他调用 未定义的行为.

因此,由于您没有传递 long,而是传递 int,因此您必须获取 int。如果两种类型具有相同的表示,则故障可能不会被注意到(正如您所怀疑的那样)。

然而,反过来也不行:如果推了更大的 long,则采用更小的 int。然而,对于典型的实现,只有在获取下一个参数时才会注意到这一点。无论哪种方式,因为这都是未定义的行为,所以必须避免。

gcc 对 printf/scanf-like 格式字符串使用函数 __attribute__s 有一些支持,但是作为你的函数的调用者没有给调用者关于类型的提示,您对编译器支持一无所知(它应该怎么知道?)。

像您提供的功能一样是引起骚乱的程序的常见来源,最好避免,因为它们很容易出现印刷错误,就像您现在注意到的一样。最好将结构数组传递给适当的函数或调用固定参数函数。它们通常是程序员为每一行代码而战的时代的放射性遗产,无论是 运行-时间还是大小。

一种替代方法可能是 C11 使用宏 _Generic 为各种参数类型调用固定大小的函数。

如果您的编译器支持 C99,您可以将可变参数函数更改为接受作为 compound literal 提供的单个参数的函数。这样的文字可以是未指定长度的数组,因此您可以执行以下操作:

#include <stdio.h>

typedef struct NameAndLong {
    const char* name;
    long value;
} NameAndLong;

long varargsExample(NameAndLong things[]);

int main(int argc, char **argv)
{
    varargsExample((NameAndLong[]){
      {"TestInt", 0},
      {"TestIntNegative", -1},
      {"TestLong", 0},
      {"TestSomethingBig",1L<<62},
      {"TestLongNegative", -1},
      {NULL}});
    return 0;
}

long varargsExample(NameAndLong things[])
{
    const char * name;
    long nextValue;
    while ((name = things->name) != 0)
    {
        nextValue = things++->value;
        printf("Got [%s] with value [%ld]\n", name, nextValue);
    }
    return 0;
}

当然,您必须替换所有的函数调用,但如果您不替换一个,编译器会告诉您,因为会有明显的原型不匹配。然后您就不必担心人们添加新电话而忘记添加 L.

就我个人而言,我发现额外的配对大括号有助于提高易读性,但铸件有点笨重。您可能想使用宏,但要注意宏参数由 圆括号 中没有包含的任何逗号分隔这一事实。大括号不算。您可以通过使用可变参数 macro:

来解决该问题
#define VA_EXAMPLE(...) (varargsExample((NameAndLong[]){__VA_ARGS__}))
// ...
VA_EXAMPLE({"TestInt", 0},
           {"TestIntNegative", -1},
           {"TestLong", 0},
           {NONE});

这里是live on ideone(用long long代替long,以配合编译环境。)