C中的未定义行为是否会将派生结构上运行的函数指针转换为基类型上运行的函数?

问题描述 投票:2回答:1

假设我们在C中具有以下众所周知的继承模式,具有父,派生子struct和vtable:

struct parent;

struct vtable {
    void(*op)(struct parent *obj);
};

struct parent {
    struct vtable *vtable;
};

struct child {
    struct parent parent;
    int bar;
};

然后,struct child的vtable通常按以下方式定义:

void child_op(struct parent *obj) {
    // Downcast
    struct child *child = (struct child *)obj;
    /* Do stuff */
}

static struct vtable child_vtable = {
    .op = child_op,
};

然后我们可以使用vtable并进行动态调度:

void foo(struct parent *obj) {
    obj->vtable->op(obj);
}

然而,在某些情况下,我们知道struct child的具体类型,例如:直接在分配结构之后。在这种情况下,通过vtable跳过间接函数调用并直接为op调用struct child的特定实现是有意义的。但是,为此,我们必须首先转换指针。 “upcast”可以在没有实际演员的情况下进行。

void bar(void) {
    struct child *obj = malloc(sizeof(*obj));
    obj->parent.vtable = &child_vtable;
    // Need convert pointer to child into pointer to parent first
    child_op(&obj->parent);
}

然而,声明child_op函数采用struct child *的参数而不是基本类型struct parent *是合法的,因为我们知道,它只会被指向struct child对象的指针调用并转换为指定给vtable的函数指针:

void child_op(struct child *obj) {
    // No need to cast here anymore
    /* Do stuff */
}

// Cast only when creating the vtable
// Is this undefined behaviour?
static struct vtable child_vtable = {
    .op = (void (*)(struct parent *))child_op,
};

void foo(struct child *obj) {
    // No need to do any conversion here anymore
    child_op(obj);
}

正如人们所看到的,这将使我们不必投射child_op并将struct child *转换为struct parent *,我们确信,我们有一个有效的指针指向struct child(并且没有指向例如struct grandchild的指针)。

我讨论了一个类似的问题here,我从中得出结论,如果void (*)(struct parent *)void (*)(struct child *)类型是兼容的,那将是合法的。

它当然与我测试的编译器和平台一起工作。如果它确实是未定义的行为,是否有任何情况,这实际上会导致问题,而失踪的演员实际上会做些什么?是否有任何知名项目使用此方案?

编辑:你可以找到关于godbolt的例子:

c struct casting function-pointers undefined-behavior
1个回答
1
投票

不幸的是,没有一种有效的方法来引用具有struct child *参数的函数,该参数带有指向struct parent *参数的函数的指针。当然我们都知道它可以用于任何常见的实现,因为在汇编语言级别,指向函数的指针是函数的地址,并且指向struct的指针,指向其第一个成员的指针是否为运。

但正是因为它是一个无操作,没有理由为了避免它而编写UB代码。因此,正确的方法是使用精确声明的签名为vtable编写函数,并将指针向下转换为正确的指向类型作为函数中的第一条指令。如果函数必须与子参数一起存在,只需在vtable中使用一个小包装器。


另一种符合方法的方法是对vtable指针使用K&R样式声明,也称为C11的n1570草案中的空参数列表。 6.7.6.3§15说:

...如果一个类型具有参数类型列表而另一个类型由函数声明符指定,该函数声明符不是函数定义的一部分并且包含空标识符列表,则参数列表不应具有省略号终止符和类型每个参数应与应用默认参数促销产生的类型兼容。

这意味着,如果您声明:

struct vtable {
    void(*op)();              // empty parameter list in declaration
};

op可以指出

void child_op(struct child *obj) {
    // No need to cast here anymore
    /* Do stuff */
}

如果你宣布child为:

struct child {
    struct parent parent;
    int bar;
};

以下代码是合法的C:

struct vtable child_vtable = { &child_op };
struct child foo = { { &child_vtable }, 25 };
foo.parent.vtable->op(&foo);

因为op实际上是用struc child *参数调用的,所以它确实与child_op兼容。

不幸的是,这有两个主要缺点:

  • 你肯定会失去对参数数量和类型的任何编译时控制(这就是原型发明的原因......)
  • 它显然是一个过时的功能:

6.11.6函数声明符 1使用带有空括号的函数声明符(不是prototype-format参数类型声明符)是一个过时的功能。

TL / DR在函数开头的向下转换并不是一件很糟糕的事情,它可以帮助你避免将来编译器复仇......

© www.soinside.com 2019 - 2024. All rights reserved.