函数指针转换为不同的签名

时间:2008-10-09 19:33:48

标签: c pointers

我使用函数指针结构来实现不同后端的接口。签名非常不同,但返回值几乎都是void,void *或int。


struct my_interface {
    void  (*func_a)(int i);
    void *(*func_b)(const char *bla);
    ...
    int   (*func_z)(char foo);
};

但后端不需要支持每个接口函数的功能。所以我有两种可能性,第一种选择是在每次调用之前检查指针是否为NULL。我不太喜欢这样,因为可读性和因为我担心性能影响(但我没有测量它)。另一种选择是使用虚函数,在极少数情况下,接口函数不存在。

因此我需要为每个签名都使用一个虚函数,我想知道是否有可能只有一个用于不同的返回值。并将其投射到给定的签名。


#include <stdio.h>

int nothing(void) {return 0;}

typedef int (*cb_t)(int);

int main(void)
{
    cb_t func;
    int i;

    func = (cb_t) nothing;
    i = func(1);

    printf("%d\n", i);

    return 0;
}

我用gcc测试了这段代码,但它确实有效。但它是否理智?或者它可以破坏堆栈还是会导致其他问题?

编辑:感谢所有答案,经过一番深入阅读后,我现在学到了很多关于调用约定的知识。现在,我们可以更好地了解幕后发生的事情。

7 个答案:

答案 0 :(得分:13)

根据C规范,转换函数指针会导致未定义的行为。事实上,有一段时间,GCC 4.3预发布版会在你输出一个函数指针时返回NULL,完全有效的指针,但是他们在发布前退出了这个更改,因为它打破了很多程序。

假设GCC继续执行它现在所做的事情,它将与默认的x86调用约定(以及大多数架构上的大多数调用约定)一起工作,但我不会依赖它。在每个调用点上针对NULL测试函数指针并不比函数调用昂贵得多。如果你真的想要,你可以写一个宏:

#define CALL_MAYBE(func, args...) do {if (func) (func)(## args);} while (0)

或者你可以为每个签名设置不同的虚函数,但我可以理解你想要避免这种情况。

修改

查尔斯贝利打电话给我,所以我去了解细节(而不是依靠我的空洞记忆)。 C specification

  

766指向一种类型的函数的指针可以转换为指向另一种类型的函数的指针,然后再返回;
  767结果应比较等于原始指针   768如果转换的指针用于调用类型与指向类型不兼容的函数,则行为未定义。

和GCC 4.2预发布(这是4.3之前的解决方式)遵循这些规则:函数指针的强制转换不会导致NULL,正如我写的那样,但是试图通过不兼容的类型调用函数,即

func = (cb_t)nothing;
func(1);
从您的示例中

将导致abort。他们改回4.1行为(允许但警告),部分原因是这次改变打破了OpenSSL,但同时修复了OpenSSL,这是编译器可以随时自由更改的未定义行为。

OpenSSL只是向其他函数类型转换函数指针,并且返回相同数量的相同精确值的值,并且这(假设您没有处理浮点)恰好在所有平台上都是安全的我所知道的召集惯例。但是,其他任何事情都可能不安全。

答案 1 :(得分:2)

我怀疑你会得到一个未定义的行为。

你可以指定(使用适当的强制转换)一个指向函数的指针到另一个指针以使用不同的签名起作用,但是当你调用它时可能会发生奇怪的事情。

你的nothing()函数没有参数,对编译器来说这可能意味着他可以优化堆栈的使用,因为那里没有参数。但是在这里你用一个参数调用它,这是一个意想不到的情况,它可能会崩溃。

我无法找到标准中的正确点,但我记得它说你可以转换函数指针,但是当你调用结果函数时,你必须使用正确的原型,否则行为是未定义的。

作为旁注,您不应该将函数指针与数据指针(如NULL)进行比较,因为指针可能属于单独的地址空间。 C99标准中有一个附录允许这个特定情况,但我认为它没有得到广泛实施。也就是说,在只有一个地址空间的架构中,将一个函数指针转换为数据指针或将其与NULL进行比较,通常可以正常工作。

答案 2 :(得分:1)

您确实存在导致堆栈损坏的风险。话虽如此,如果你使用extern "C"链接声明函数(和/或__cdecl,具体取决于你的编译器),你可能能够逃脱这一点。它类似于printf()之类的函数可以在调用者自行决定的情况下获取可变数量的参数。

在当前情况下这是否有效还可能取决于您使用的确切编译器选项。如果您正在使用MSVC,那么调试与发布编译选项可能会产生很大的不同。

答案 3 :(得分:0)

应该没问题。由于调用者负责在调用后清理堆栈,因此不应在堆栈上留下任何额外的内容。被调用者(在这种情况下没有())是好的,因为它不会尝试使用堆栈上的任何参数。

编辑:这确实假设cdecl调用约定,这通常是C的默认值。

答案 4 :(得分:0)

只要您可以保证使用调用者平衡堆栈而不是被调用者(__cdecl)的方法进行调用。如果您没有指定调用约定,则可以将全局约定设置为其他约定。 (__stdcall或__fastcall)这两种情况都可能导致堆栈损坏。

答案 5 :(得分:0)

除非您使用特定于实现的/特定于平台的内容来强制执行正确的调用约定,否则这将无效。对于某些调用约定,被调用函数负责清理堆栈,因此它们必须知道已经推送了什么。

我会检查NULL然后调用 - 我无法想象它会对性能产生任何影响。

计算机可以像他们所做的那样快地检查NULL。

答案 6 :(得分:-1)

C标准明确不支持将函数指针强制转换为NULL。你受编译器编写者的支配。它适用于很多编译器。

对于C来说,最大的烦恼之一是函数指针没有等效的NULL或void *。

如果您真的希望您的代码是防弹的,您可以声明自己的空值,但每个函数类型需要一个。例如,

void void_int_NULL(int n) { (void)n; abort(); }

然后你可以测试

if (my_thing->func_a != void_int_NULL) my_thing->func_a(99);

丑陋,不是吗?