C语言中的接口
我正在设计一个应用程序,遇到了一个实现问题。我有以下结构定义:C语言中的接口,c,struct,language-lawyer,undefined-behavior,C,Struct,Language Lawyer,Undefined Behavior,我正在设计一个应用程序,遇到了一个实现问题。我有以下结构定义: app.h: struct application_t{ void (*run_application)(struct application_t*); void (*stop_application)(struct application_t*); } struct application_t* create(); 当我试图“实现”这个应用程序时,问题出现了。我倾向于定义另一个结构: app.c: struct
app.h
:
struct application_t{
void (*run_application)(struct application_t*);
void (*stop_application)(struct application_t*);
}
struct application_t* create();
当我试图“实现”这个应用程序时,问题出现了。我倾向于定义另一个结构:
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)代码>yeilds UB
如果struct application*
,则应用程序ptr的“静态类型”,而“动态类型”是struct tcp\u application*
。N1570 6.2.7(p1)不兼容结构应用程序和结构tcp应用程序
其成员之间应进行一对一的通信,如:
每对对应的成员都声明为兼容
类型
在这种情况下,这显然是不正确的
你能提供一个解释行为的标准参考吗?你的两个结构不兼容,因为它们是不同的类型。您已经找到了“兼容类型”一章,该章定义了使两个结构兼容的因素。根据6.5/7,当您使用指向错误类型的指针访问这些结构时,会出现UB,严格违反别名
解决这一问题的显而易见的方法是:
struct tcp_application_impl_t{
struct application_t app;
int client_fd;
int socket_fd;
}
现在,这些类型可能是别名,因为tcp\u application\u impl\t
是一个聚合,在其成员中包含application\u t
另一种方法是使用隐藏在C17 6.5.2.3/6中的“联合公共初始序列”的特殊规则来明确定义:
为了简化工会的使用,有一个特别的保证:如果工会包含
几个结构共享一个共同的初始序列(见下文),如果联合
对象当前包含其中一个结构,允许它检查公共
它们中任何一个的起始部分,即完整的联合类型声明
是可见的。两个结构共享一个相同的初始序列,如果对应的成员
对于一个或多个初始成员的序列,具有兼容类型(对于位字段,具有相同的宽度)
这将允许您在声明原始类型时使用它们。但在同一翻译单元中的某个地方,您必须添加一个伪union typedef以利用上述规则:
typedef union
{
struct application_t app;
struct tcp_application_impl_t impl;
} initial_sequence_t;
你不需要实际使用这个联合体的任何实例,它只需要在那里可见。这会告诉编译器,这两种类型可以使用别名,只要它们的初始序列相同。在您的例子中,它指的是函数指针,而不是tcp\u application\u impl\t
中的尾随变量
编辑:
免责声明。常见的初始序列技巧显然有点争议,编译器用它做的事情超出了委员会的预期。在C和C++中可能不同。参见如果“严格别名规则”(N1570 6.5p7)仅被解释为指定事物可能出现别名的情况(鉴于脚注88,这似乎是作者的意图,脚注88中说“本列表的目的是指定对象可能出现或不出现别名的情况”)如果在使用两种不同类型的左值访问对象的所有上下文中,所涉及的左值中的一个显然是从另一个新派生出来的,那么像您这样的代码应该不会带来任何问题
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别名
,也不会对此类代码产生问题,但因为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别名
方言或icc、gcc和clang处理的-fno严格别名
方言应适合您的目的。gcc和clang的-fstrict别名
方言应该被认为不适合您的目的,不值得瞄准。看起来像是“C语言中的继承”