【问题标题】:Interfaces in CC中的接口
【发布时间】:2019-02-07 09:33:41
【问题描述】:

我正在设计一个应用程序,但遇到了一个实施问题。我有以下结构定义:

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); yields UB。

app_ptr 的“静态类型”如果struct application_t*,但“动态类型”是struct tcp_application_impl_t*struct application_tstruct tcp_application_t 与 N1570 6.2.7(p1) 不兼容:

他们的成员之间应该有一对一的通信,例如 每对对应的成员都声明为 compatible 类型

在这种情况下显然不是真的。

您能否提供解释该行为的标准参考?

【问题讨论】:

  • 看起来像“C 中的继承”黑客......也许将结构的第一个字段定义为 application_t 类型会更好
  • 你必须使用struct tcp_application_impl_t { application_t base; int client_fd, socket_fd; },如果是cp_application_impl_t *p,那么在二进制级别上使用&p->base == p
  • @Jean-FrançoisFabre 差不多,是的。我实际上不确定这是否是一种“常见的 C 方式”来做这样的事情......
  • 我认为你需要 &app_ptr->base; in create() 并使用 void run_application(application_t* p) { tcp_application_impl_t*q = CONTAINING_RECORD(p, tcp_application_impl_t, base); ...} where #define CONTAINING_RECORD(address, type, field) ((type *)( (PCHAR)(address) - (ULONG_PTR)(&((type *)0)->field)))
  • 行为,正如您正确指出的那样,是未定义的。该标准没有解释未定义的行为。

标签: c struct language-lawyer undefined-behavior


【解决方案1】:

您的两个结构不兼容,因为它们是不同的类型。您已经找到了“兼容类型”一章,它定义了使两个结构兼容的原因。当您使用指向错误类型的指针访问这些结构时,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": Why does C (99+), but not C++, stipulate a 'visible declaration of the union type'?

【讨论】:

  • 酷!您确定规范所讨论的“检查”不必通过 union 类型的实例吗?
  • @unwind 相当肯定,因为这个技巧在生产代码中到处使用。我自己更喜欢前一个版本,其中继承的结构包含其基类的实例。
  • @unwind 实际上,这似乎是一个有争议的“烫手山芋”,会产生各种编译器缺陷报告。见stackoverflow.com/questions/34616086/…。唔。尽管显然有一些 DR,但它在 C17 中从未更改过。
  • @Lundin 应用 6.2.5(p2): All pointers to structure types shall have the same representation and alignment requirements as each other 意味着指向所有结构的所有指针类型都相互兼容。应用6.7.2.1(p15): A pointer to a structure object, suitably converted, points to its initial member (or if that member is a bit-field, then to the unit in which it resides), and vice versa. 如果tcp_application_impl_t 包含application_t 作为它的第一个成员,我们可以简单地将tcp_application_impl_t 转换为application_t 并返回。对吗?
  • @SomeName 您可以安全地从tcp_application_impl_t 转到application_t。 6.7.2.1 和 6.5/7(严格别名)都支持它。你不能做的,这可能是不言而喻的,是分配一个application_t,将一个指针投射到tcp_application_impl_t,然后访问实际上不存在的其他成员。
【解决方案2】:

如果“严格别名规则”(N1570 6.5p7) 仅被解释为指定事物可能别名的情况(这似乎是作者的意图,因为脚注 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 也不会有别名。即使在启用-fstrict-aliasing 的情况下,像“icc”这样的编译器也不会出现此类代码的问题,但是因为 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 精神包括“信任程序员”和“不要阻止程序员做需要做的事情”的原则。除非有特殊需要来处理不特别适合人们所做的实现的限制,否则应该以适合自己目的的方式针对维护 C 精神的实现。 icc 处理的-fstrict-aliasing 方言,或icc、gcc 和clang 处理的-fno-strict-aliasing 方言应该适合您的用途。 gcc 和 clang 的 -fstrict-aliasing 方言应该被认为根本不适合您的目的,并且不值得定位。

【讨论】:

  • 如果我理解正确-fstrict-aliasing 应该是不合适的。但是如果将struct application_t 声明为第一个成员,我们可以安全地将两个指针相互转换,因为如果正确对齐,指向结构的指针可以转换为其第一个元素的指针(结构指针具有相同的对齐要求所以它们是兼容的)。
  • @SomeName:强制转换可能是安全的,但 gcc 和 clang 将 Strict Alaising 规则解释为禁止几乎任何需要获取联合对象地址或使用指针强制转换来实现类似语义的构造.
猜你喜欢
  • 2011-04-20
  • 1970-01-01
  • 2017-06-30
  • 2011-04-10
  • 1970-01-01
  • 2023-04-03
  • 1970-01-01
  • 2013-04-11
相关资源
最近更新 更多