我正在将32位应用程序转换为64位的过程中,我正在运行的痛点之一是期望长但可能传递整数的可变函数,例如参数被硬编码为-1而不是-1L,源于长尺寸的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 read改为将值读取为int,无论传递的是int还是long,这似乎都可以工作,例如:
nextValue = va_arg(args, int);
这是一个碰巧工作的hack,还是C规范中有什么东西使它始终如一地工作?请注意,这个应用程序可以在Unix/Linux和Windows上运行,在Windows上,long是32位,所以我不担心函数传递的值不能用32位整数表示。我创建了一个基本的单元测试,通过INT_MIN -> INT_MAX将整数/long的混合传递给可变函数,并读取为va_arg(args, int),它似乎可以工作(在AIX, Solaris和RHEL上测试),但我不确定这是否只是在这些平台上工作的未定义行为。
正确的修复方法是识别这个函数的所有调用者,并确保他们在所有情况下都传递一个long,但是这些函数的使用相当广泛/很难识别,没有编译器的支持来识别。我试图看到作为一个替代方案,如果有一个GCC扩展,我可以利用它来指定自定义可变类型检查,类似于格式参数检查(sprintf, printf等)。
编译器不知道variadic函数从列表中获取的是哪种类型,因此它只能依赖给定的参数类型。它对实参执行默认的实参提升。
对于整数类型,这基本上将"较小"类型提升到int
或unsigned
,而将int
/unsigned
和"较大"类型保持不变。
当获取参数时,你有责任从可变参数中获取正确的类型。其他任何内容都会调用未定义行为。
因此,由于您没有传递long
,而是传递int
,因此您必须获取int
。如果这两种类型具有相同的表示,那么错误可能不会被注意到(正如您所怀疑的)。
然而,另一种方式也不应该起作用:如果一个较大的long
被推入,则取一个较小的int
。然而,对于典型的实现,只有在获取下一个参数时才会注意到这一点。无论哪种方式,由于这都是未定义的行为,因此必须避免。
gcc对printf
/scanf
类格式字符串使用__attribute__
函数提供了一些支持,但是由于函数的调用者没有向调用者提供关于类型的提示,因此您丢失了编译器支持(它应该如何知道?)像您所呈现的这样的函数是骚乱程序的常见来源,最好避免使用,因为它们容易出现与您现在注意到的相同的排版错误。最好将struct数组传递给适当的函数或调用固定参数函数。它们通常是程序员为每一行代码而奋斗的时代遗留下来的放射性遗产,无论是运行时还是大小。
另一种选择可能是C11使用宏,_Generic
为各种参数类型调用固定大小的函数。
如果您的编译器支持C99,您可以将可变函数更改为接受作为复合文字提供的单个参数的函数。这些字面值可以是未指定长度的数组,因此您可以执行以下操作:
#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
。
就我个人而言,我发现额外的成对括号有助于提高可读性,但是石膏有点笨重。您可能想要使用宏,但要注意这样一个事实:宏参数是用逗号分隔的,而不是用括号括起来的。大括号不算。你可以通过使用可变的宏:
来解决这个问题。#define VA_EXAMPLE(...) (varargsExample((NameAndLong[]){__VA_ARGS__}))
// ...
VA_EXAMPLE({"TestInt", 0},
{"TestIntNegative", -1},
{"TestLong", 0},
{NONE});
这是ideone上的实时显示(使用long long
而不是long
,以匹配编译环境)