【问题标题】:C: Cast void (*)(void *) to void (*)(char *)C: 将 void (*)(void *) 转换为 void (*)(char *)
【发布时间】:2020-10-24 00:13:23
【问题描述】:

如果我尝试使用实际的 void (*)(void *) 调用带有参数 void (*)(char *) 的函数,我会收到此错误:

note: expected ‘void (*)(char *)’ but argument is of type ‘void (*)(void *)’

是否允许将void (*)(void *) 转换为void (*)(char *)?如果不是,为什么不呢? 如果它是安全的,如何在不抑制所有铸造错误的情况下抑制错误。

谢谢!

【问题讨论】:

  • 请给我们看一个实际通话的例子。为什么不在调用中添加 argument
  • 类型转换很少是“安全的”。通常,将较大的类型转换为char* 或将任何类型转换为void* 并返回到相同的类型可能是唯一一般“安全”的转换。看起来您正在转换函数指针类型。这可能会导致问题,具体取决于您所在平台的 ABI。例如,在这种情况下,可能有一个压入堆栈的参数永远不会弹出,因为被调用者不需要任何参数。

标签: c casting function-pointers


【解决方案1】:

我同意应该在需要void (*)(char *) 时传入void (*)(void *) 类型的实际参数时是“赋值兼容性”。

对于某些人来说,这可能看起来很奇怪,因为void * 是不太具体的类型,因此不应允许您将其分配给char * 类型的参数或变量。但在这种情况下,情况恰恰相反。这有时被描述为“逆变”。最好用一个例子来说明。

#include <stdio.h>
#include <string.h>

char globalData[10];

void PassHello(void (*func)(char *))
{
    char *text = "hello";
    func(text);
}

void CopyData(void *source)
{
    memcpy(globalData, source, sizeof globalData);
}

void CopyHello(void)
{
    PassHello(CopyData);
}   

int main()
{
    CopyHello();
    puts(globalData);
}

func(text) 拨打CopyData 应该没有问题。即使CopyData 被声明为接受void * 作为参数,它也应该接受更具体的类型,在本例中为char *

这是违反直觉的,但确实如此:void (*)(void *) 作为一种类型void (*)(char *) 更具体就是逆变的意义所在。

不幸的是,C 语言不关心逆变,只允许对裸void * 进行隐式指针转换。编译器可以将其作为语言扩展来实现,但当前的主要编译器没有。

因此,上面的代码给出了类型错误。

注意: 正如其他海报所指出的那样,类型错误是由 C 标准不保证不同指针类型兼容这一事实证明的。 C 编译器可以根据指针的类型随意生成不同的代码。从理论上讲,这也可以用于“嵌套”类型(在这种情况下,函数指针的参数类型),但编译器的复杂性可能会爆炸。所以在某处画一条线是有意义的; C 不是 Haskell。

我认为最简洁的解决方案(即不涉及显式类型转换)是围绕 void (*)(void *) 函数指针创建一个“包装器”函数。例如:

void CopyCharData(char *source)
{
    CopyData(source);
}

然后改为传递包装器:

PassHello(CopyCharData);

这将编译并运行得很好。

现场演示: https://repl.it/repls/ImmediateWindingOrders

【讨论】:

  • 好吧,C 编译器不支持它,因为 C 语言不允许逆变。 C没有任何模式匹配类型的规则。像 C++ 这样的函数参数类型,它唯一允许的隐式指针转换是与裸 void* 之间的转换(C++ 禁止)。所有其他指针转换都需要强制转换。
  • @cmaster-reinstatemonica 感谢您指出这一点。我应该更清楚地说,我指的逆变不是一种语言特性,而是一个概念,以证明 OP 的场景是类型安全的。逆变不是 C 语言设计的一部分这一事实并不意味着该场景在 C 中是类型不安全的。您可以使用像包装函数这样简单的东西来解决类型错误这一事实证明了我的观点。包装函数不是 hack;这是一个完全正常的语言功能。
  • 在这种情况下,我建议您将“不幸的是,大多数(全部?)C 编译器不支持逆变”变成类似“不幸的是,C 语言不关心逆变,允许隐式指针仅适用于裸 void* 的转换。编译器可以将其作为语言扩展来实现,但当前的主要编译器没有”。
  • @cmaster-reinstatemonica 谢谢;我非常喜欢你的建议,我逐字逐句编辑了它。
  • @cmaster-reinstatemonica 只是检查...您提到“仅允许对裸 void* 进行隐式指针转换”,这似乎表明您的印象是我的解决方案依赖于 C 允许从char *void * 的隐式向下转换。请注意不是;这些都是 all 向上转型。它同样适用于其他强类型语言。
【解决方案2】:

当您传递声明为函数指针的函数参数的参数时,C 标准中的约束要求参数是指向兼容函数类型的指针(或为空指针常量)。对于要兼容的函数类型,它们声明的参数类型必须兼容。但是char *void *是不同的类型,互不兼容。

事实上,C 标准要求 char *void * 具有相同的表示形式,并允许它们轻松地来回转换,但它仍然指定它们不兼容。除此之外,这些类型规则有助于捕捉错误——当程序员错误输入了他们想要使用的名称或表达式时,如果类型不匹配,编译器可能会捕捉到它。

因此,void (*)(char *)void (*)(void *) 不兼容,并且,如果您尝试在预期使用另一个的地方使用一个,编译器会警告您。

您可以使用显式转换将其中一个指针转换为另一种类型。将参数传递给函数时,如果满足某些规则,C 标准的规则将隐式地将参数转换为参数类型。但是,使用强制转换显式请求的转换规则更广泛。您可以将任何函数指针强制转换为任何函数指针类型。因此,如果 xvoid (*)(void *),您可以将其转换为 (void (*)(char *)) x,然后您可以将其传递给声明为具有该类型的参数。

但是,您可以进行这种转换这一事实并不能保证指针在用于调用函数时会起作用。允许转换函数指针的规则主要是为了允许将函数指针临时转换为另一种类型,以某种常见的形式存储在传递中,然后在用于调用函数之前转换回其原始类型。允许转换的规则并没有说转换后的函数指针如果用于调用带有不兼容参数的函数。

【讨论】:

  • @RuudHelderman:无论您希望 C 应该是什么,使用转换后的指针都不是类型安全的。如果使用指向void ()(void *) 的指针来调用void ()(char *),则行为不是由C 标准定义的,并且不能依赖C 实现在这种情况下的行为方式,因为没有来自C 实现或其他来源。您关于它是类型安全的断言可能基于某个 C 实现如何表现的模型,您没有说明,但 C 标准不使用该模型,这个结论是不合理的。
  • 感谢您的澄清,我不知道这一点。 @GiuseppeGuerrini 也对潜在风险做了一些解释。我想这意味着在两个函数指针类型之间进行显式类型转换是不安全的。我想知道当编译器实际上使用“不兼容”指针时,还有多少大型 C 项目仍然可以工作。
  • @RuudHelderman:它们工作正常,直到优化器根据类型兼容性做出积极的决策。我们无法再根据底层机器架构来推断实现将如何表现,因为优化器正在根据 C 标准给出的理论模型做出决策。即使 C 实现使用带有特定类型指针的机器指令(平面地址空间,所有类型都相同),优化器也可能导致 C 实现表现得好像不是这样。
【解决方案3】:

这两种函数指针类型不等价的原因是调用约定可能不同。 C 不假定 void *char * 参数以相同的方式传递(尽管在大多数处理器上它们都这样做)。理论上,这些类型甚至可以有不同的编码。所以我们不能假设一个期望 char * 的函数和一个期望 void * 的函数共享相同的调用约定。这就是编译器抱怨的原因。
“你这个骗子!C 确实让我将 char * 传递给一个需要 void * 的函数!我已经做了很多年了!”。
C 保证可以在void * 中安全地转换所有指针(注意:原则上,转换可能需要一些处理),并且在传递参数时它还应用从some *void * 的自动转换, 前提是编译器知道参数的原始类型和函数的实际签名。如果函数由指针调用,编译器必须从携带函数指针的变量类型中获取函数的签名。在您的情况下,变量是void (*)(char *),因此编译器将生成代码以传递char *,而无需首先将其转换为void *,正如实际函数所期望的那样。这是未定义行为的情况。

【讨论】:

  • void *char * 不能有不同的编码。 C 2018 6.2.5 28 说“指向void 的指针应具有与指向字符类型的指针相同的表示和对齐要求。”脚注 49 说:“相同的表示和对齐要求意味着作为函数的参数、函数的返回值和联合成员的可互换性。”后者虽然不规范,但告诉我们应该能够传递char *,即使未声明参数(不在原型中或在... 部分中,函数也需要void * )。
  • 从“实用”的角度来看,您是对的,但这取决于“工会成员的可互换性”的确切含义。在一种非常严格的解释中,人们可能会认为这仅意味着两种类型都需要相同的存储空间。但更合理的解释(如你的)是我们可以做 "myunion.myvoid=somepchar; strcpy(myunion.mypchar, "xxx");"并获得与“strcpy(somepchar,"XXX");”相同的效果。据我所知,这就是所有现代处理器上实际发生的情况。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-12-20
  • 1970-01-01
  • 2012-09-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多