C 中的接口
Interfaces in C
我在设计应用程序时遇到了实施问题。我有以下结构定义:
app.h
:
struct application_t{
void (*run_application)(struct application_t*);
void (*stop_application)(struct application_t*);
}
struct application_t* create();
当我尝试 "implement" 这个 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);
会产生 UB。
app_ptr的"static type"如果struct application_t*
,但是"dynamic type"是struct tcp_application_impl_t*
。 struct application_t
和 struct tcp_application_t
与 N1570 6.2.7(p1) 不兼容:
there shall be a one-to-one correspondence between their members such
that each pair of corresponding members are declared with compatible
types
在这种情况下显然不是这样。
能否请您提供对解释该行为的标准的参考?
您的两个结构不兼容,因为它们是不同的类型。您已经找到了 "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 中的偷偷摸摸的特殊规则 "union common initial sequence":
One special guarantee is made in order to simplify the use of unions: if a union contains
several structures that share a common initial sequence (see below), and if the union
object currently contains one of these structures, it is permitted to inspect the common
initial part of any of them anywhere that a declaration of the completed type of the union
is visible. Two structures share a common initial sequence if corresponding members
have compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members.
这将允许您使用您声明的原始类型。但是在同一个翻译单元的某个地方,你必须添加一个虚拟联合类型定义来利用上面的规则:
typedef union
{
struct application_t app;
struct tcp_application_impl_t impl;
} initial_sequence_t;
您不需要实际使用这个联合的任何实例,它只需要坐在那里可见。这告诉编译器这两种类型允许别名,只要它们的公共初始序列就可以。在您的情况下,这意味着函数指针而不是 tcp_application_impl_t
.
中的尾随变量
编辑:
免责声明。常见的初始序列技巧显然有点争议,编译器用它做了委员会预期之外的其他事情。并且在 C 和 C++ 中的工作方式可能不同。参见
如果 "strict aliasing rule" (N1570 6.5p7) 仅被解释为指定事物可能混叠的情况(这似乎是作者的意图,给定脚注 88,它说 "The intent of this list is to specify those circumstances in which an object may or may not be aliased") 像你这样的代码应该没有问题,前提是在使用两种不同类型的左值访问对象的所有上下文中,其中一个所涉及的左值明显是从另一个左值派生出来的。
6.5p7 唯一有意义的方法是,如果涉及从其他对象新鲜可见地派生的对象的操作被识别为对原始对象的操作。然而,何时承认这种推导的问题留作实施质量问题,并且认为市场比委员会能够更好地判断什么是适合 "quality" 实施的必要条件一些特定的目的。
如果目标是编写可在配置为遵守脚注 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;
然后 p1
的第二次使用将在 i==j
的情况下成为 p2
的别名,因为 p2
将通过方式访问与 p1
关联的存储不涉及 p1
,在形成 p1
和最后一次使用它之间(因此别名 p1
)。
根据标准的作者,C 的精神包括原则 "Trust the programmer" 和 "Don't prevent the programmer from doing what needs to be done"。除非特别需要处理不是特别适合人们正在做的事情的实现的限制,否则应该以适合自己目的的方式瞄准支持 C 精神的实现。 icc处理的-fstrict-aliasing
方言,或者icc、gcc、clang处理的-fno-strict-aliasing
方言,应该适合你的用途。 gcc 和 clang 的 -fstrict-aliasing
方言应该被认为根本不适合您的目的,不值得瞄准。
我在设计应用程序时遇到了实施问题。我有以下结构定义:
app.h
:
struct application_t{
void (*run_application)(struct application_t*);
void (*stop_application)(struct application_t*);
}
struct application_t* create();
当我尝试 "implement" 这个 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);
会产生 UB。
app_ptr的"static type"如果struct application_t*
,但是"dynamic type"是struct tcp_application_impl_t*
。 struct application_t
和 struct tcp_application_t
与 N1570 6.2.7(p1) 不兼容:
there shall be a one-to-one correspondence between their members such that each pair of corresponding members are declared with compatible types
在这种情况下显然不是这样。
能否请您提供对解释该行为的标准的参考?
您的两个结构不兼容,因为它们是不同的类型。您已经找到了 "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 中的偷偷摸摸的特殊规则 "union common initial sequence":
One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible. Two structures share a common initial sequence if corresponding members have compatible types (and, for bit-fields, the same widths) for a sequence of one or more initial members.
这将允许您使用您声明的原始类型。但是在同一个翻译单元的某个地方,你必须添加一个虚拟联合类型定义来利用上面的规则:
typedef union
{
struct application_t app;
struct tcp_application_impl_t impl;
} initial_sequence_t;
您不需要实际使用这个联合的任何实例,它只需要坐在那里可见。这告诉编译器这两种类型允许别名,只要它们的公共初始序列就可以。在您的情况下,这意味着函数指针而不是 tcp_application_impl_t
.
编辑:
免责声明。常见的初始序列技巧显然有点争议,编译器用它做了委员会预期之外的其他事情。并且在 C 和 C++ 中的工作方式可能不同。参见
如果 "strict aliasing rule" (N1570 6.5p7) 仅被解释为指定事物可能混叠的情况(这似乎是作者的意图,给定脚注 88,它说 "The intent of this list is to specify those circumstances in which an object may or may not be aliased") 像你这样的代码应该没有问题,前提是在使用两种不同类型的左值访问对象的所有上下文中,其中一个所涉及的左值明显是从另一个左值派生出来的。
6.5p7 唯一有意义的方法是,如果涉及从其他对象新鲜可见地派生的对象的操作被识别为对原始对象的操作。然而,何时承认这种推导的问题留作实施质量问题,并且认为市场比委员会能够更好地判断什么是适合 "quality" 实施的必要条件一些特定的目的。
如果目标是编写可在配置为遵守脚注 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;
然后 p1
的第二次使用将在 i==j
的情况下成为 p2
的别名,因为 p2
将通过方式访问与 p1
关联的存储不涉及 p1
,在形成 p1
和最后一次使用它之间(因此别名 p1
)。
根据标准的作者,C 的精神包括原则 "Trust the programmer" 和 "Don't prevent the programmer from doing what needs to be done"。除非特别需要处理不是特别适合人们正在做的事情的实现的限制,否则应该以适合自己目的的方式瞄准支持 C 精神的实现。 icc处理的-fstrict-aliasing
方言,或者icc、gcc、clang处理的-fno-strict-aliasing
方言,应该适合你的用途。 gcc 和 clang 的 -fstrict-aliasing
方言应该被认为根本不适合您的目的,不值得瞄准。