澄清 C 和 C++ 中结构的 ODR 规则差异

Clarification on difference in ODR rules for structs in C and C++

我知道 ODR、链接、staticextern "C" 如何与函数一起工作。但是我不确定类型的可见性,因为它们不能被声明 static 并且 C 中没有匿名名称空间。

特别是,如果编译为 C 和 C++,我想知道以下代码的有效性

// A.{c,cpp}
typedef struct foo_t{
    int x;
    int y;
} Foo;

static int use_foo() 
{ 
    Foo f;
    f.x=5;
    return f.x;
}
// B.{c,cpp}
typedef struct foo_t{
    double x;
} Foo;

static int use_foo() 
{ 
    Foo f;
    f.x=5.0;
    return f.x;// Cast on purpose
}

使用以下两个命令(我知道两个编译器都会根据扩展名自动检测语言,因此名称不同)。

版本 8.3 顺利编译,没有任何错误。显然,如果两个结构符号都有外部链接,则存在 ODR 违规,因为定义不相同。是的,编译器不需要报告它,因此我的问题是因为两者都没有。

它是有效的 C++ 程序吗?

我不这么认为,这就是匿名命名空间的用途。

它是有效的 C 程序吗?

我在这里不确定,我读过类型被认为是 static 这将使程序有效。有人可以确认一下吗?

C、C++ 兼容性

如果这些定义在 public 头文件中,可能在不同的 C 库中,并且一个 C++ 程序包括这两者,每个也在不同的 TU 中,那会是 ODR 吗?如何防止这一情况? extern "C"有什么作用吗?

对于C。 该程序有效。此处适用的唯一要求是“严格别名规则”,即对象只能通过兼容类型的左值访问(+ 6.5p7 中描述的一些例外)。

structures/unions 在单独翻译单元中定义的兼容性在 6.2.7p1 中定义。

... two structure, union, or enumerated types declared in separate translation units are compatible if their tags and members satisfy the following requirements: If one is declared with a tag, the other shall be declared with the same tag. If both are completed anywhere within their respective translation units, then the following additional requirements apply: there shall be a one-to-one correspondence between their members such that each pair of corresponding members are declared with compatible types; if one member of the pair is declared with an alignment specifier, the other is declared with an equivalent alignment specifier; and if one member of the pair is declared with a name, the other is declared with the same name. For two structures, corresponding members shall be declared in the same order. For two structures or unions, corresponding bit-fields shall have the same widths. For two enumerations, corresponding members shall have the same values.

因此示例中的结构不兼容。

但是,这不是问题,因为 f 对象是通过本地定义的类型创建和访问的。如果对象是用一个翻译单元中定义的 Foo 类型创建的,并通过另一个翻译单元中的其他 Foo 类型访问,则将调用 UB:

// A.c
typedef struct foo_t{
    int x;
    int y;
} Foo;

void bar(void *f);

void foo() 
{ 
    Foo f;
    bar(&f);
}

// B.c
typedef struct foo_t{
    double x;
} Foo;

// using void* to avoid passing pointer to incompatible types
void bar(void *f_) 
{ 
    Foo *f = f_;
    f->x=5.0; // UB!
}

我将用于 C++ 语言的参考 the n1570 draft for C11 for the C language and the draft n4860 for C++20

  1. C语言

    类型在 C 中没有链接:6.2.2 标识符的链接§6:

    The following identifiers have no linkage: an identifier declared to be anything other than an object or a function...

    这意味着 a.c 和 b.c 中使用的类型是不相关的:您在两个编译单元中正确声明了不同的对象。

  2. C++ 语言

    类型在 C++ 中确实有链接。 6.6 程序和链接 [basic.link] 说(强调我的):

    • §2:

    A name is said to have linkage when it might denote the same object, reference, function, type, template, namespace or value as a name introduced by a declaration in another scope

    • §4

    An unnamed namespace or a namespace declared directly or indirectly within an unnamed namespace has internal linkage. All other namespaces have external linkage. A name having namespace scope that has not been given internal linkage above and that is the name of
    ...
    a named class...
    ...
    has its linkage determined as follows:
    — if the enclosing namespace has internal linkage, the name has internal linkage;
    — otherwise, if the declaration of the name is attached to a named module (10.1) and is not exported (10.2), the name has module linkage;
    — otherwise, the name has external linkage

    a.cpp 和 b.cpp 中声明的类型与外部链接共享相同的标识符并且不兼容:程序格式错误。


也就是说,大多数常见的编译器都能够编译 C 或 C++ 源代码,我敢打赌他们会努力共享这两种语言的大部分实现。出于这个原因,我相信现实世界的实现即使对于 C++ 语言也能产生预期的结果。但是未定义的行为并不禁止预期的结果...

其他答案指出这是 C++ 格式错误的程序。

实际上,如果您在单独的翻译单元中有两个单独的(非静态)void foo(bar); 定义,则重载函数可能会出现 link 错误。 我希望这是(部分)为什么 C++ 有这样的规则,即(某些)类型具有外部 linkage.

如果类型是真正私有的,它们就不会发生冲突。但是它们会以相同的方式命名,因为如果两个 TU do 具有相同的类型 bar 定义(例如通过 .h 或手动复制),它们需要解决调用相同的函数。

// A.cpp
typedef struct foo{  // names ending with _t are reserved
    int x;
    int y;
} Foo;

int take_foo(Foo f) {
    return f.x;
}

int main(){}  // so it's linkable without special options like -nostdlib and linker entry-point defaults
// B.{c,cpp}
typedef struct foo{
    double x;
} Foo;

double take_foo(Foo f) {
    return f.x;
}

以防万一,这些函数将在某些目标上编译为不同的机器代码,包括我测试过的 x86-64 System V ABI。 (第一个 double arg 已经在 return 值寄存器中,即使在只包含一对双打的结构中也是如此。但与 ARM64 和其他一些 RISC 不同,第一个整数 arg 是 not 在 return 值寄存器中传递,因此 ret 之前需要一个 mov。)

$ g++ [AB].cpp
/usr/bin/ld: /tmp/ccM89kvx.o: in function `take_foo(foo)':
B.cpp:(.text+0x0): multiple definition of `take_foo(foo)'; /tmp/cckZ5qRG.o:A.cpp:(.text+0x0): first defined here
collect2: error: ld returned 1 exit status

如果函数或结构标签具有不同的名称,则不会出现错误。 (是的,我在禁用优化的情况下编译,并且没有 link 时间优化,所以没有机会在它们发生冲突之前删除未使用的函数。)

但是,仅更改 typedef 名称 而不 更改结构标记是不够的。这就说得通了;相同类型的所有 typedef 都需要解析为相同的 asm 名称,因此即使您不直接使用它,GCC 也会基于结构标记进行处理。请注意 linker 错误消息将其分解回 take_foo(foo) 而不是 Foo

我没有通过标准措辞来查看两个 typedef ... Foo 在 ISO C++ 中是否合法,尽管在实际 C++ 实现中这不是问题。

创建任一函数 static 也可以解决问题,因为 static 函数具有相同的 asm 名称是可以的。

如果编译为 C,这也会有一个 linker 错误,它没有函数重载,所以在同一个程序中有两个非静态 take_foo 函数已经是一个问题不管它们的参数是否是相同标记名的结构。