在C中防止不兼容的函数转换?

时间:2015-05-10 23:01:51

标签: c casting callback

有时在没有。

的情况下强制转换函数回调是很有用的

例如,我们可能有复制某些数据的功能:
struct MyStruct *my_dupe_fn(const struct MyStruct *s)

但是将其作为通用回调传递:
typedef void *(*MyGenericCopyCallback)(void *key);

例如:
ensure_key_in_set(my_set, my_key, (MyGenericCopyCallback)my_dupe_fn);

由于const struct MyStruct *void *之间的差异在这种情况下不会导致问题,因此它不会导致任何错误(至少在函数调用中它自己)

但是,如果稍后将某个参数添加到my_dupe_fn,则可能会导致不会给出编译器警告的错误。

有没有办法转换函数,但如果参数或返回值的大小不同,仍会显示警告?

强制性免责声明:当然C不是安全的*,但是在广泛使用的语言中防止潜在错误的方法仍然有用。

4 个答案:

答案 0 :(得分:3)

你说"不会导致任何错误"但是它会导致未定义的行为通过具有不兼容的返回类型或参数类型的函数指针调用函数,即使在示例代码中也是如此。

如果您想依赖未定义的行为,那么您将承担风险。依赖UB迟早会导致错误。更好的想法是重新设计回调接口,而不是依赖于未定义的行为。例如,仅使用正确类型的函数作为回调函数。

在您的示例中,这可能是:

typedef void *MyCallback(void *key);    // style: avoid pointer typedefs

struct MyStruct *my_dupe_fn(const struct MyStruct *s)
{ ... }

void *my_dupe_fn_callback(void *s)
{
     return my_dupe_fn(s);
}

void generic_algorithm(MyCallback *callback)
{
    // ....
    ensure_key_in_set(my_set, my_key, callback); 
    // ....
}

// elsewhere
generic_algorithm(my_dupe_fn_callback);  

注意缺少演员阵容。管理不使用任何函数强制转换的样式策略比允许某些类型的策略更简单。

答案 1 :(得分:2)

如果您使用的是gcc而且不怕使用有用的扩展程序,那么您可以查看plan9-extensions。结合匿名结构字段(标准自C99)作为第一个字段,它们允许构建具有静态函数等的类型层次结构。避免在我的代码中使用大量的强制转换并使其更具可读性。

不确定,但根据gcc文档,MS编译器也支持一些(所有?)这些功能。但是,不保证这一点。

答案 2 :(得分:1)

后来的错误来自两段代码,它们说同一个东西不同步 - 第一个定义my_dupe_fn的类型,第二个是你将泛型回调指针强制转换回原始类型。

这就是DRY(不要重复自己)的用武之地。重点是只说一次,这样你就不能再回来改变一个实例了。

在这种情况下,您需要输入指向my_dupe_fn的指针类型,最好非常接近您声明函数本身的位置,以帮助确保typedef始终随函数签名本身一起更改。 / p>

编译器永远不会为你捕获这个,只要它认为它只是处理一个通用的void指针。

答案 3 :(得分:0)

不幸的是,如果你使用C,你通常不得不放弃一些编译时的安全性。你可能会得到最好的警告,但是如果你的设计是以这种方式统一地投射函数指针,你很可能忽略或彻底禁用它们。相反,您希望将重点放在实现安全编码标准上。你不能用武力保证,你可以通过政策强烈鼓励。

我建议,如果你能负担得起,首先要建立参数并返回值而不是整个函数指针。灵活的表示是这样的:

typedef void* GenericFunction(int argc, void** args);

这模拟了具有可变回调的能力,并且您可以在调试版本中统一执行运行时安全检查,例如,以确保参数的数量与假设相符:

void* MyCallback(int argc, void** args)
{
    assert(argc == 2);
    ...
    return 0;
}

如果您需要比传递的单个参数更安全,并且通过稍微庞大的结构化解决方案可以为每个参数提供额外指针的典型成本,您可以执行以下操作:

struct Variant
{
    void* ptr;
    const char* type_name;
};

struct Variant to_variant(void* ptr, const char* type_name)
{
    struct Variant new_var;
    new_var.ptr = ptr;
    new_var.type_name = type_name;
    return new_var;
}

void* from_variant(struct Variant* var, const char* type_name)
{
     assert(strcmp(var->type_name, type_name) == 0 && "Type mismatch!");
     return var->ptr;
}

void* pop_variant(struct Variant** args, const char* type_name)
{
     struct Variant* var = *args;
     assert(var->ptr && "Trying to pop off the end of the argument stack!");
     assert(strcmp(var->type_name, type_name) == 0 && "Type mismatch!");
     ++*args;
     return var->ptr;
}

使用像这样的宏:

#define TO_VARIANT(val, type) to_variant(&val, #type);
#define FROM_VARIANT(var, type) *(type*)from_variant(&var, #type);
#define POP_VARIANT(args, type) *(type*)pop_variant(&args, #type);

typedef struct Variant* GenericFunction(struct Variant* args);

示例回调:

struct Variant* MyCallback(struct Variant* args)
{
    // `args` is null-terminated.
    int arg1 = POP_VARIANT(args, int);
    float arg2 = POP_VARIANT(args, float);
    ...
    return 0;
}

通过这些MyCallback字段追踪type_name时,您可以在调试器中看到一个附带好处。

如果你的代码库支持回调为动态类型的脚本语言,那么这种事情会很有用,因为脚本语言不应该在他们的代码中进行类型转换(通常它们意味着更安全一些)。然后,类型名称可用于使用这些type_name字段自动将参数转换为脚本语言的本机类型。