C语言中的接口

3
我正在设计一个应用程序,遇到了一个实现问题。我有以下结构体定义:
在 `app.h` 中:
struct application_t{
    void (*run_application)(struct application_t*);
    void (*stop_application)(struct application_t*);
}

struct application_t* create();

问题出现在我尝试“实现”这个application_t时。我倾向于定义另一个结构体:
app.c:
struct tcp_application_impl_t{
    void (*run_application)(struct application_t*);
    void (*stop_application)(struct application_t*);
    int client_fd;
    int socket_fd;
}

struct application_t* create(){
     struct tcp_application_impl_t * app_ptr = malloc(sizeof(struct tcp_application_impl_t));
     //do init
     return (struct application_t*) app_ptr;
}

所以,如果我按照以下方式使用它:
#include "app.h"

int main(){
    struct application_t *app_ptr = create();
    (app_ptr -> run_application)(app_ptr);    //Is this behavior well-defined?
    (app_ptr -> stop_application)(app_ptr);   //Is this behavior well-defined?
}

我困惑的问题是,如果我调用 (app_ptr -> run_application)(app_ptr); 会不会产生未定义行为。

app_ptr 的“静态类型”是 struct application_t*,但“动态类型”是 struct tcp_application_impl_t*。根据 N1570 6.2.7(p1) 的规定,struct application_tstruct tcp_application_t 不兼容:

它们的成员之间应该存在一对一的对应关系,使得每一对相应的成员都声明了兼容的类型

在这种情况下,显然不符合上述要求。

请问您能提供一份标准文件,解释这种行为吗?


1
看起来像是 C 语言中的继承 hack... 或许将你的结构体的第一个字段定义为 application_t 类型会更好。 - Jean-François Fabre
3
你必须使用 struct tcp_application_impl_t { application_t base; int client_fd, socket_fd; },如果有变量 tcp_application_impl_t *p,则在二进制级别上 &p->base == p。请注意,翻译过程中不能改变原文意思,也不要在结果中包含解释或其他内容。 - RbMm
这个链接可能会有所帮助:https://dev59.com/hnM_5IYBdhLWcg3ww2Kb#1237302 - Jean-François Fabre
1
我认为你需要在create()中使用&app_ptr->base;,并且使用void run_application(application_t* p) { tcp_application_impl_t*q = CONTAINING_RECORD(p, tcp_application_impl_t, base); ...},其中#define CONTAINING_RECORD(address, type, field) ((type *)( (PCHAR)(address) - (ULONG_PTR)(&((type *)0)->field))) - RbMm
1
行为,正如您所正确指出的那样,是未定义的。标准没有解释未定义的行为。 - n. m.
显示剩余3条评论
2个回答

2
你的两个结构体不兼容,因为它们是不同类型的。你已经找到了定义两个结构体兼容性的章节"compatible types"。当你使用错误类型的指针访问这些结构体时,UB就会出现,这违反了6.5/7中的严格别名规则。
解决这个问题的明显方法应该是:
struct tcp_application_impl_t{
    struct application_t app;
    int client_fd;
    int socket_fd;
}

现在类型可能会发生别名,因为tcp_application_impl_t是一个聚合体,其中包含一个application_t作为其成员之一。
另一种使此定义明确的替代方法是使用C17 6.5.2.3/6中隐藏的“联合通用初始序列”的诡秘特殊规则:
“为了简化联合的使用,提供了一项特殊保证:如果一个联合包含几个结构体,这些结构体共享一个公共初始序列(见下文),并且如果联合对象当前包含其中一个结构体,则允许在任何声明联合的完成类型可见的地方检查它们中任何一个的公共初始部分。如果对应成员具有兼容类型(对于位字段来说,具有相同的宽度)的一个或多个初始成员的序列,则两个结构体共享一个公共初始序列。”
这将允许您按照原始声明使用您的原始类型。但是,在同一翻译单元中的某个地方,您必须添加一个虚拟联合typedef来利用上述规则:
typedef union
{
  struct application_t app;
  struct tcp_application_impl_t impl;
} initial_sequence_t;

你不需要真正使用此联合的任何实例,它只需存在于可见位置。这告诉编译器,就其共同初始序列而言,这两种类型允许别名。在你的情况下,这意味着函数指针而不是tcp_application_impl_t中的尾随变量。
编辑:免责声明。共同初始序列技巧显然有点具争议性,编译器做的事情可能与委员会的意图不同。在C和C++中可能有所不同。请参见Union 'punning' structs w /“common initial sequence”:为什么C(99+),但不是C ++,规定了公开声明联合类型?

酷!你确定规范所说的“检查”不需要通过union类型的实例进行吗? - unwind
@unwind 相当确定此技巧在生产代码中被使用。我个人更喜欢前一版本,其中继承的结构包含其基类的实例。 - Lundin
@unwind 实际上,这似乎是一个有争议的“热土”,导致了各种编译器缺陷报告。请参见https://dev59.com/s1sW5IYBdhLWcg3wwZcP。嗯。即使存在一些DR,它在C17中也从未改变过。 - Lundin
应用6.2.5(p2)规定:所有指向结构类型的指针应具有相同的表示和对齐要求,这意味着所有结构体的指针类型彼此兼容。应用6.7.2.1(p15)规定:适当转换后,指向结构体对象的指针将指向其初始成员(如果该成员是位域,则指向其所在的单元),反之亦然。如果tcp_application_impl_t包含application_t作为其第一个成员,则可以简单地将tcp_application_impl_t强制转换为application_t,然后再转回来。这样做正确吗? - Some Name
1
@SomeName,你可以安全地从tcp_application_impl_t转换为application_t。这在6.7.2.1和6.5/7(严格别名)中都得到了支持。但是,你不能分配一个application_t,将指针强制转换为tcp_application_impl_t,然后访问实际上不存在的其他成员,这一点可能不用说。 - Lundin
显示剩余5条评论

1

如果将“严格别名规则”(N1570 6.5p7)仅解释为指定可能发生别名的情况(鉴于脚注88的内容,“此列表的目的是指定对象可能或不可能发生别名的情况”,这似乎是作者的意图),那么像您的代码这样的代码应该没有问题,只要在访问对象时使用两种不同类型的lvalue,在所有上下文中,涉及的其中一个lvalue明显是从另一个lvalue派生的。

只有涉及到从其他对象明显新派生的对象的操作被认为是对原始对象的操作时,6.5p7才有任何意义。然而,何时才能认识到这种派生是一个实现质量问题,而市场比委员会更能判断哪些因素是某个特定目的的“优质”实现所必需的。

如果目标是编写适用于配置为遵循脚注88明确意图的实现的代码,则只要对象不发生别名,就应该是安全的。保持这一要求可能需要确保编译器可以看到指针彼此相关,或者它们在使用点各自新地从共同对象派生。例如:

thing1 *p1 = unionArray[i].member1;
int v1 = p1->x;
thing2 *p2 = unionArray[j].member2;
p2->x = 31;
thing1 *p3 = unionArray[i].member1;
int v2 = p3->x;

每个指针都将在从unionArray新获取时的上下文中使用,即使i==j也不会有别名问题。像“icc”这样的编译器即使启用了-fstrict-aliasing选项,也不会对这种代码产生问题,但是因为gcc和clang即使在不涉及别名问题的情况下也要求程序员遵循6.5p7的规定,所以它们无法正确处理此代码。

请注意,如果代码是这样的:

thing1 *p1 = unionArray[i].member1;
int v1 = p1->x;
thing2 *p2 = unionArray[j].member2;
p2->x = 31;
int v2 = p1->x;

然后,在i==j的情况下,p1的第二次使用将别名p2,因为p2将访问与p1相关联的存储器,通过不涉及p1的方式,在p1形成和最后一次使用之间(从而别名p1)。
根据标准的作者,C的精神包括原则“信任程序员”和“不要阻止程序员做需要做的事情”。除非有特殊需要应对实现的限制,这些实现不太适合正在进行的工作,否则应针对实现以适合自己目的的方式支持C的精神。由icc处理的-fstrict-aliasing方言或由icc、gcc和clang处理的-fno-strict-aliasing方言应适合您的目的。应该认识到,gcc和clang的-fstrict-aliasing方言简单地不适合您的目的,也不值得针对。

如果我理解正确的话,-fstrict-aliasing 应该是不合适的。但是,如果我们将 struct application_t 声明为第一个成员,我们可以安全地将这两个指针彼此转换,因为如果正确对齐,结构体指针可以转换为其第一个元素的指针(结构体指针具有相同的对齐要求,因此它们是兼容的)。 - Some Name
1
@某个名字:强制类型转换可能是安全的,但gcc和clang将严格别名规则解释为禁止几乎任何需要获取联合对象地址或使用指针转换来实现类似语义的构造。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接