Home > Blockchain >  Why don't structures in headers violate ODR across multiple translation units?
Why don't structures in headers violate ODR across multiple translation units?

Time:06-20

From what I understand, the main reason people separate function declarations and definitions is so that the functions can be used in multiple compilation units. So then I was wondering, what's the point of violating DRY this way, if structures don't have prototypes and would still cause ODR problems across compilation units? I decided to try and define a structure twice using a header across two compilation units, and then combining them, but the code compiled without any errors.

Here is what I did:

main.c:

#include "test.h"

int main() {
    return 0;
}

a.c:

#include "test.h"

test.h:

#ifndef TEST_INCLUDED
#define TEST_INCLUDED

struct test {
    int a;
};

#endif

Then I ran the following gcc commands.

gcc -c a.c
gcc -c main.c
gcc -o final a.o main.o

Why does the above work and not give an error?

CodePudding user response:

C's one definition rule (C17 6.9p5) applies to the definition of a function or an object (i.e. a variable). struct test { int a; }; does not define any object; rather, it declares the identifier test as a tag of the corresponding struct type (6.7.2.3 p7). This declaration is local to the current translation unit (i.e. source file) and it is perfectly fine to have it in several translation units. For that matter, you can even declare the same identifier as a tag for different types in different source files, or in different scopes, so that struct test is an entirely different type in one file / function / block than another. It would probably be confusing, but legal.

If you actually defined an object in test.h, e.g. struct test my_test = { 42 };, then you would be violating the one definition rule, and the behavior of your program would be undefined. (But that does not necessarily mean you will get an error message; multiple definitions are handled in various different ways by different implementations.)

CodePudding user response:

The key section in the standard is nearly indigestible, but §6.2.7 Compatible type and composite type covers the details, with some forward references:

¶1 Two types have compatible type if their types are the same. Additional rules for determining whether two types are compatible are described in 6.7.2 for type specifiers, in 6.7.3 for type qualifiers, and in 6.7.6 for declarators.55) Moreover, 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.

¶2 All declarations that refer to the same object or function shall have compatible type; otherwise, the behavior is undefined.

¶3 A composite type can be constructed from two types that are compatible; it is a type that is compatible with both of the two types and satisfies the following conditions:

  • If both types are array types, the following rules are applied:
    • If one type is an array of known constant size, the composite type is an array of that size.
    • Otherwise, if one type is a variable length array whose size is specified by an expression that is not evaluated, the behavior is undefined.
    • Otherwise, if one type is a variable length array whose size is specified, the composite type is a variable length array of that size.
    • Otherwise, if one type is a variable length array of unspecified size, the composite type is a variable length array of unspecified size.
    • Otherwise, both types are arrays of unknown size and the composite type is an array of unknown size.
  • The element type of the composite type is the composite type of the two element types.
  • If only one type is a function type with a parameter type list (a function prototype), the composite type is a function prototype with the parameter type list.
  • If both types are function types with parameter type lists, the type of each parameter in the composite parameter type list is the composite type of the corresponding parameters.

These rules apply recursively to the types from which the two types are derived.

¶4 For an identifier with internal or external linkage declared in a scope in which a prior declaration of that identifier is visible,56) if the prior declaration specifies internal or external linkage, the type of the identifier at the later declaration becomes the composite type.

55) Two types need not be identical to be compatible.

56) As specified in 6.2.1, the later declaration might hide the prior declaration.

Emphasis added

The second part of ¶1 covers explicitly the case of structures, unions and enumerations declared in separate translation units. It is crucial to allowing separate compilation. Note footnote 55 too. However, if you use the same header to define a given structure (union, enumeration) in separate translation units, the chances of you not using a compatible type are small. It can be done if there is conditional compilation and the conditions are different in the two translation units, but you usually have to be trying quite hard to run into problems.

  • Related