C++ / a working model

78 / 163   ·   C++17   ·   12 min

ODR, Header Definitions, and What inline Actually Does

Keep this sentence

A header can be included by multiple translation units, but definitions in it must satisfy the one-definition rule. inline mainly addresses definitions and entity identity across translation units; it does not force calls to be expanded. Template visibility, name lookup, and consistent build macros are likewise part of interface correctness.

In this lesson
  1. First Distinguish Declarations, Definitions, and Entities
  2. What inline Allows, and What It Does Not Promise
  3. Why Template Definitions Often Live in Headers
  4. From a Single-File Example to a Multi-Translation-Unit Experiment
  5. Example
  6. Exercise

First Distinguish Declarations, Definitions, and Entities

A declaration tells the compiler what a name is; a definition also supplies a complete description of a function body, object, or type. For an ordinary non-inline, non-template external function, the program needs a unique definition that satisfies the ODR; an object or function that is odr-used cannot have only a declaration. Header guards only prevent repeated expansion in the same translation unit; they cannot stop a.cpp and b.cpp from each producing a definition.

For example, write extern int counter; in a header and int counter = 0; in a unique state.cpp so multiple callers access the same object. If you put extern int counter = 0; with an initializer into a header, it is still a definition. Do not casually use static to silence a linker error: it usually turns shared state into a separate object in each translation unit.

What inline Allows, and What It Does Not Promise

An external inline function in a traditional header, and an inline variable since C++17, may have qualifying definitions in different translation units and still denote the same entity, with the same address. The definitions must use the same token sequence, and related name lookup must usually refer to the same entities; textual similarity alone is not enough. If macros cause different source files to see different function bodies, the rule can be silently violated.

inline does not require the machine code to be inlined; a compiler can also expand functions that are not marked inline. A member function defined inside a class is implicitly inline in ordinary header-file usage, and a constexpr function is also implicitly inline. In-class definitions that belong to a named module follow different rules; do not apply header-file experience to modules unconditionally. An inline definition must also be reachable in every translation unit that needs it.

Why Template Definitions Often Live in Headers

Implicit instantiation usually needs to see the template definition; a function-template declaration alone is not enough to generate a specialization for an arbitrary type. You can therefore put the complete definition of larger from the example below into a header; it need not be marked inline merely so multiple translation units can use it. Ordinary class definitions and template definitions can also appear in different translation units under the ODR's conditions.

If the set of supported types is fixed, you can hide the definition in an implementation file that provides explicit instantiation definitions, and use extern template in the header to declare those instantiations and suppress duplicate instantiation. The cost is that a new type cannot be used freely; a matching instantiation must be added. An explicit full specialization of a function template does not automatically inherit inline from the original template; if its definition is placed in a header it must be handled separately, or moved to a unique implementation file.

From a Single-File Example to a Multi-Translation-Unit Experiment

The example below is meant to show an interface and a shared object; running it as a single file cannot by itself verify cross-file identity. For a real experiment, move the namespace guide block into a guide.hpp that has include guards; put from_a and from_b into a.cpp and b.cpp respectively, both of which include that header. Have main.cpp include it and declare unsigned* from_a(); unsigned* from_b();, keeping main and the required standard headers.

Run g++ -std=c++17 -Wall -Wextra -pedantic main.cpp a.cpp b.cpp -o odr-demo, then run ./odr-demo. The two pointers should be equal, and both increments should act on the same counter. Build organization must unify the public header and the options that affect definitions; the linker having merged symbols does not mean it has proved that the whole program satisfies the ODR.

Pitfalls

  • A static variable at namespace scope in a header produces a separate object in each translation unit; that is not equivalent to a C++17 external inline variable, and it changes address and state-sharing semantics.
  • An ODR violation across translation units is usually an ill-formed program that does not require a diagnostic; one successful link and one correct output do not prove that the definitions are consistent.

Run an example

Minimum C++17 · complete program · Download .cpp

#include <cassert>
#include <iostream>

namespace guide {
inline unsigned visits = 0;

inline unsigned next() {
    return ++visits;
}

template<class T>
T larger(T a, T b) {
    return a < b ? b : a;
}
}

unsigned* from_a() { return &guide::visits; }
unsigned* from_b() { return &guide::visits; }

int main() {
    assert(from_a() == from_b());
    const auto first = guide::next();
    const auto second = guide::next();
    assert(first == 1u && second == 2u);
    assert(*from_a() == 2u);
    assert(guide::larger(3, 7) == 7);
    std::cout << guide::visits << '\n';
}

Compile locally

g++ -std=c++17 -Wall -Wextra -Wpedantic -pthread tooling-odr.cpp -o example && ./example

Expected result

2

CHECK YOUR UNDERSTANDING

Close the answer. Explain it.

Prepare a C++17 header shared by a.cpp and b.cpp that contains int limit = 10; and int twice(int x) { return x * 2; }. How do you change them into a shared variable and a function that may be defined in a header? What if you must support C++11?

Show a reference answer

For C++17 write inline int limit = 10; and inline int twice(int x) { return x * 2; }, add include guards, and ensure every translation unit sees a consistent definition. Calls to twice must still stay in the range where the product is representable as int; inline does not change the arithmetic rules. For C++11 change the variable to extern int limit; in the header, include that header in one .cpp and write int limit = 10; there; the function may still be inline. Do not make the variable static, or you will get two copies of the state.

Check the sources

Drafts and official chapters change. The version mark is only the example’s minimum.

Back to the catalog