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

在C中防止不兼容的函数强制转换?,c,casting,callback,C,Casting,Callback,有时,不使用强制转换函数回调是很有用的 例如,我们可能有一个复制某些数据的函数: struct MyStruct*my\u dupe\u fn(const struct MyStruct*s) 但将其作为一般回调传递: typedef void*(*MyGenericCopyCallback)(void*键) 例如:确保密钥在密钥集中(我的密钥集,我的密钥,(MyGenericCopyCallback)我的副本fn) 由于在这种情况下,const struct MyStruct*和void*之间

有时,不使用强制转换函数回调是很有用的

例如,我们可能有一个复制某些数据的函数:
struct MyStruct*my\u dupe\u fn(const struct MyStruct*s)

但将其作为一般回调传递:
typedef void*(*MyGenericCopyCallback)(void*键)

例如:
确保密钥在密钥集中(我的密钥集,我的密钥,(MyGenericCopyCallback)我的副本fn)

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

但是,如果以后将参数添加到
my\u dupe\u fn
,这可能会导致一个bug,不会发出编译器警告


是否有一种方法可以强制转换函数,但如果参数或返回值大小不同,仍会显示警告




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

后面的错误来自两段代码,它们说的是同一件事情不同步——第一段代码定义了my_dupe_fn的类型,第二步是将泛型回调指针转换回其原始类型

这就是干(不要重复你自己)的原因。关键是只说一次,这样你以后就不会回来只改变一个实例了

在本例中,您希望typedef指向my_dupe_fn的指针的类型,最好是非常接近您声明函数本身的位置,以帮助确保typedef始终随函数符号本身而变化


只要编译器认为它只是处理一个通用的void指针,它就永远不会为您捕捉到这一点。

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

不确定,但根据gcc文档,MS编译器也支持一些(全部?)这些特性。但是,对此不作任何保证。

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

如果你想依靠未定义的行为,那就是你的风险。依赖UB迟早会导致bug。更好的办法是重新设计回调接口,使其不依赖于未定义的行为。例如,仅使用正确类型的函数作为回调函数

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

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);  

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

不幸的是,如果使用C,您通常不得不放弃一些编译时安全性。您最多可能会收到一条警告,但如果您有一个设计是以这种方式统一强制转换函数指针,您可能会忽略或完全禁用它们。相反,您希望将重点放在实现安全编码标准上。你不能用武力保证的,你可以用政策大力鼓励

如果您负担得起的话,我建议您从强制转换参数和返回值开始,而不是从整个函数指针开始。灵活的表示方式如下所示:

typedef void* GenericFunction(int argc, void** args);
#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);
这模拟了具有可变回调的能力,并且您可以在调试构建中统一执行运行时安全检查,例如,确保参数的数量与假设相匹配:

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;
}
对于这样的宏:

typedef void* GenericFunction(int argc, void** args);
#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;
}
一个附带的好处是,当您通过那些
type\u name
字段跟踪
MyCallback
时,可以在调试器中看到


如果您的代码库支持回调到动态类型的脚本语言中,这种事情可能会很有用,因为脚本语言不应该在其代码中执行类型转换(通常它们是为了更安全一点)。然后,可以使用这些
type\u name
字段,使用类型名称自动将参数动态转换为脚本语言的本机类型。

您可以使用类型定义的“contract”签名编写
my\u dupe\u fn
,并将函数中的参数转换为所需类型。那么你就不需要演员阵容了,你会对那些潜在的未来变化保持“稳健”。一个很好的例子是DWORD ThreadProc(void*参数)
。我从未见过有人试图将不同类型的函数强制转换为泛型版本。通常人们在
ThreadProc
中将参数强制转换为他们期望的类型。有几种方法可以防止某人在C中做一些愚蠢的事情,而这不是其中之一。这在某种程度上是演员阵容的重点,强制告诉编译器“我在这方面,不必担心你”。任何代码指针都可能被迫戴上这种伪装。最终,调用方有责任提供正确的接口。如果他们强迫投球,他们就把球扔了。他们应该编写适当的匹配函数,而不是精确地转换fptr,以便在接口更改时捕获这种情况。不确定,但是:是否确实需要转换
my\u dupe\u fn
?void*可以在不强制转换的情况下分配给任何其他指针类型,但我不确定这是否适用于此间接寻址。@Matt McNabb,
typedef void*(*MyGenericCopyCallback)(void*键)
只是函数s的typedef