在C中,将变量函数指针强制转换为带有限参数的函数指针是否安全?

时间:2012-07-30 23:33:56

标签: c undefined-behavior variadic-functions calling-convention

我想创建一个函数指针,该函数指针将处理带有变量参数列表的函数的子集的子集。该用例是一个函数,它将...带到一个带有特定参数列表的函数,因此您可以处理变量参数而无需处理va_list和朋友。

在下面的示例代码中,我将带有变量参数的函数转换为具有硬编码参数列表的函数(反之亦然)。这工作(或恰好工作),但由于使用的调用约定,我不知道是否是巧合。 (我在两个不同的基于x86_64的平台上试过它。)

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

void function1(char* s, ...)
{
    va_list ap;
    int tmp;

    va_start(ap, s);
    tmp = va_arg(ap, int);
    printf(s, tmp);
    va_end(ap);
}

void function2(char* s, int d)
{
    printf(s, d);
}

typedef void (*functionX_t)(char*, int);
typedef void (*functionY_t)(char*, ...);

int main(int argc, char* argv[])
{
    int x = 42;

    /* swap! */
    functionX_t functionX = (functionX_t) function1;
    functionY_t functionY = (functionY_t) function2;

    function1("%d\n", x);
    function2("%d\n", x);
    functionX("%d\n", x);
    functionY("%d\n", x);

    return 0;
}

这是未定义的行为吗?如果是的话,任何人都可以提供一个不起作用的平台示例,或者给出一个更复杂的用例,以一种方式调整我的示例以使其失败的方法吗?


编辑:为了解决此代码会破坏更复杂的参数的问题,我扩展了我的示例:

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

struct crazy
{
    float f;
    double lf;
    int d;
    unsigned int ua[2];
    char* s;
};

void function1(char* s, ...)
{
    va_list ap;
    struct crazy c;

    va_start(ap, s);
    c = va_arg(ap, struct crazy);
    printf(s, c.s, c.f, c.lf, c.d, c.ua[0], c.ua[1]);
    va_end(ap);
}

void function2(char* s, struct crazy c)
{
    printf(s, c.s, c.f, c.lf, c.d, c.ua[0], c.ua[1]);
}

typedef void (*functionX_t)(char*, struct crazy);
typedef void (*functionY_t)(char*, ...);

int main(int argc, char* argv[])
{
    struct crazy c = 
    {
        .f = 3.14,
        .lf = 3.1415,
        .d = -42,
        .ua = { 0, 42 },
        .s = "this is crazy"
    };


    /* swap! */
    functionX_t functionX = (functionX_t) function1;
    functionY_t functionY = (functionY_t) function2;

    function1("%s %f %lf %d %u %u\n", c);
    function2("%s %f %lf %d %u %u\n", c);
    functionX("%s %f %lf %d %u %u\n", c);
    functionY("%s %f %lf %d %u %u\n", c);

    return 0;
}

它仍然有效。任何人都可以指出何时失败的具体例子?

$ gcc -Wall -g -o varargs -O9 varargs.c
$ ./varargs
this is crazy 3.140000 3.141500 -42 0 42
this is crazy 3.140000 3.141500 -42 0 42
this is crazy 3.140000 3.141500 -42 0 42
this is crazy 3.140000 3.141500 -42 0 42

2 个答案:

答案 0 :(得分:8)

转换指向不同函数指针类型的指针是完全安全的。但是语言唯一保证的是你以后可以将它转换回原始类型并获得原始指针值。

通过强制转换为不兼容函数指针类型的指针调用函数会导致未定义的行为。这适用于所有不兼容的函数指针类型,无论它们是否是可变参数。

您发布的代码会产生未定义的行为:不是在演员阵容中,而是在通话时。


试图追逐“它会失败的地方”的例子是毫无意义的努力,但无论如何它应该很容易,因为参数传递约定(低级和语言级别)都有很大的不同。例如,下面的代码通常不会在实践中“起作用”

void foo(const char *s, float f) { printf(s, f); }

int main() {
  typedef void (*T)(const char *s, ...);
  T p = (T) foo;
  float f = 0.5;
  p("%f\n", f);
}

打印零而不是0.5(GCC)

答案 1 :(得分:4)

是的,这是未定义的行为。

它恰好正常工作,因为指针正在为您当前的编译器,平台和参数类型排列。尝试用双打和其他类型做这个,你可能会重现奇怪的行为。

即使你不这样做,这也是非常危险的代码。

我假设你对varargs感到恼火。考虑在union或struct中定义一组通用参数,然后传递它。