将在派生结构上运行的函数指针转换为在基类型上运行的函数,是 C 语言中未定义的行为吗?
Is it undefined behavior in C to cast function pointers operating on derived structs up to functions operating on the base type?
假设我们在 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
for 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 *)
是 兼容的[=57,那将是合法的=].
它在实践 中确实适用于我测试过的编译器和平台。如果它确实是未定义的行为,是否存在任何情况,这实际上会导致问题并且丢失的演员表实际上会做些什么?有什么知名项目在用这个方案吗?
编辑:您可以在 godbolt 上找到示例:
不幸的是,没有有效的方法来引用带有 struct child *
参数的函数,并带有指向带有 struct parent *
参数的函数的指针。当然我们都知道它适用于任何常见的实现,因为在汇编语言级别,指向函数的指针是函数的地址,而将指向 struct 的指针转换为指向其第一个成员的指针是 no-操作
但正是因为它是一个空操作,所以没有理由为了避免它而编写 UB 代码。因此,正确的方法是使用准确声明的签名为 vtable 编写函数,并将指向正确指向类型的指针向下转换为函数中的第一条指令。如果函数必须与子参数一起存在,只需在 vtable 中使用一个小包装器即可。
另一种符合要求的方法是对 vtable 指针使用 K&R 样式声明,在 C11 的 n1570 草案中也称为 空参数列表 。 6.7.6.3 §15 说:
...If one type has a parameter type list and the other type is specified by a
function declarator that is not part of a function definition and that contains an empty
identifier list, the parameter list shall not have an ellipsis terminator and the type of each
parameter shall be compatible with the type that results from the application of the
default argument promotions.
这意味着如果您声明:
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 Function declarators
1 The use of function declarators with empty parentheses (not prototype-format parameter
type declarators) is an obsolescent feature.
TL/DR 函数开头的 downcast 并不是什么可怕的事情,它可以让你免于未来编译器的报复...
假设我们在 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
for 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 *)
是 兼容的[=57,那将是合法的=].
它在实践 中确实适用于我测试过的编译器和平台。如果它确实是未定义的行为,是否存在任何情况,这实际上会导致问题并且丢失的演员表实际上会做些什么?有什么知名项目在用这个方案吗?
编辑:您可以在 godbolt 上找到示例:
不幸的是,没有有效的方法来引用带有 struct child *
参数的函数,并带有指向带有 struct parent *
参数的函数的指针。当然我们都知道它适用于任何常见的实现,因为在汇编语言级别,指向函数的指针是函数的地址,而将指向 struct 的指针转换为指向其第一个成员的指针是 no-操作
但正是因为它是一个空操作,所以没有理由为了避免它而编写 UB 代码。因此,正确的方法是使用准确声明的签名为 vtable 编写函数,并将指向正确指向类型的指针向下转换为函数中的第一条指令。如果函数必须与子参数一起存在,只需在 vtable 中使用一个小包装器即可。
另一种符合要求的方法是对 vtable 指针使用 K&R 样式声明,在 C11 的 n1570 草案中也称为 空参数列表 。 6.7.6.3 §15 说:
...If one type has a parameter type list and the other type is specified by a function declarator that is not part of a function definition and that contains an empty identifier list, the parameter list shall not have an ellipsis terminator and the type of each parameter shall be compatible with the type that results from the application of the default argument promotions.
这意味着如果您声明:
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 Function declarators
1 The use of function declarators with empty parentheses (not prototype-format parameter type declarators) is an obsolescent feature.
TL/DR 函数开头的 downcast 并不是什么可怕的事情,它可以让你免于未来编译器的报复...