Skip to content
VLSI Mentor

C++ Part I · Module 24 · C++ Build and Project Architecture

Translation Units and the One-Definition Rule

Every chapter before this one treated a program as a single thing. It is not. A C++ program is compiled one translation unit at a time, each one an independent job that knows only the text the preprocessor handed it, and the pieces are joined afterwards by a linker that understands names and sizes and nothing else. Almost every build error that is not a syntax error comes from that seam. The rule governing it is the one-definition rule, and it is worth learning precisely rather than as folklore, because it produces two completely different failures. One of them is the friendliest error in C++: the linker finds two definitions of the same thing, names it, and stops. The other is the least friendly failure in the language: two translation units disagree about what a type looks like, every compilation succeeds, the link succeeds, no warning is issued at any level, and the program reads fields out of the wrong offsets. This chapter measures both, along with the exceptions the rule grants — inline, templates, class definitions — and the exception that most often gets used wrongly, which is a static variable in a header.

Intermediate19 min readC++Part IFundamentalsBuild

Part I · Module 24 · Chapter 24.1 · Translation Units and the One-Definition Rule

1. What a Translation Unit Is

A translation unit is one source file after the preprocessor has finished with it: every #include pasted in, every macro expanded, every #if resolved. That text is what the compiler actually sees, and it is the only thing it sees. The compiler does not know about your other source files, your project layout, or your build system.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
src/bus.cpp  + everything it includes  ->  one translation unit  ->  build/bus.o
src/main.cpp + everything it includes  ->  one translation unit  ->  build/main.o

Three consequences follow directly, and they explain most of the module:

A header is not a unit of compilation. It is text that gets copied into however many translation units include it. Chapter 24.4 §1 measures one small header expanding to 87,964 lines in each consumer.

Each translation unit is compiled in ignorance of the others. If two of them disagree about anything, the compiler cannot know. §4 is the measurement of what that costs.

The linker joins object files by symbol name. It sees names and addresses; it does not see your types. Chapter 24.5 is about what those names contain, and §4 is about what they leave out.

You can look at a translation unit directly, which is worth doing once:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
clang++ -std=c++20 -E src/bus.cpp | wc -l

2. The Rule, Stated Precisely

The one-definition rule has three parts, and people usually remember only the first.

One. Every variable, function, class type, enumeration and template must have exactly one definition in the whole program — with the exceptions in part three.

Two. Every translation unit may contain at most one definition of any of those. Declarations are unlimited; definitions are not.

Three. Some entities may be defined in more than one translation unit, provided every definition is token-for-token identical and the names in them mean the same things. This applies to:

  • class types
  • enumerations
  • inline functions and inline variables
  • templates, and their partial specialisations
  • constexpr functions, which are implicitly inline

That third part is what makes headers possible at all. A struct Config in a header is defined in every translation unit that includes it, and that is legal precisely because the rule permits it — on the condition that the definitions are identical. §4 is what happens when that condition is broken, and the thing to notice is that the condition is on you, not on the compiler. Nothing checks it.

One more clause matters and is easy to miss: a violation of the identical-definition requirement is undefined behaviour, and no diagnostic is required. The standard explicitly does not ask the implementation to detect it. So when §4 shows zero warnings, that is not a toolchain weakness; it is the rule working as specified.

3. The Loud Failure: Two Definitions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// a1.cpp
int shared() { return 1; }

// b1.cpp
int shared() { return 2; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
duplicate symbol 'shared()' in:
    …/odr/a1.o
    …/odr/b1.o
ld: 1 duplicate symbols
clang++: error: linker command failed with exit code 1 (use -v to see invocation)

This is the best error message in the chapter. It names the entity, demangled, and it names both object files. The build stops. Nothing wrong reaches a program.

The realistic causes are worth listing because the error is so blunt that the cause is sometimes not obvious:

  • A function defined in a header without inline, included by two translation units. This is the common one, and §6 is the fix.
  • A non-inline, non-const variable at namespace scope in a header — int verbose = 0; — which is a definition, not a declaration.
  • The same .cpp compiled into two targets that are then linked together.
  • A name you did not choose colliding with a library's, which is what namespaces exist to prevent.

The mirror-image error is the other loud one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Undefined symbols for architecture arm64:
  "configure(int)", referenced from:
      _main in sigmain.o
ld: symbol(s) not found for architecture arm64
clang++: error: linker command failed with exit code 1 (use -v to see invocation)

That means a declaration promised something no definition provided. Chapter 24.5 §3 measures the most interesting cause — a signature that drifted between the header and the source — and Chapter 24.2 §2 measures the template version of it.

4. The Silent Failure: Two Different Definitions

This is the section to remember. The same class, declared in two translation units, with one field inserted in the middle — exactly what happens when a header changes and only some objects are rebuilt:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// a3.cpp — TU A's view
struct Config { int cycles; int master; };

// b3.cpp — TU B's view (a field was inserted)
struct Config { int cycles; int resp; int master; };

Both compile. The program links. It runs:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  one buffer, two disagreeing declarations of Config:
  A wrote  cycles=412 master=2   (A thinks sizeof=8)
  B reads  cycles=412 resp=2 master=-286331154   (B thinks sizeof=12)
  sizeInA()=8  sizeInB()=12  -> the TUs DISAGREE
  link status: SUCCESS. diagnostics: NONE.

Read the second line carefully. The value 2 that A wrote into master is read by B as resp. And B's master reads -286331154, which is 0xEEEEEEEE — the byte pattern the test filled the buffer with, because B is reading four bytes past where A stopped writing.

Every field after the inserted one is shifted. A struct where the inserted field is early and the real data is late produces a program in which most values are wrong, and a struct where it is late produces one in which most values are right — which is worse, because it looks like a subtle bug in the logic rather than a build problem.

And nothing reports it. Compiling the same two translation units under -Weverything — clang's "every warning that exists" switch — and linking with -Wl,-why_load, then grepping both outputs for any mention of odr, definition or layout, returns zero matches from the compiler and zero from the linker.

-Weverything has nothing to say about this. Neither does the linker. This is not an oversight — §2's third part explains it. Each translation unit was internally consistent and individually correct; the inconsistency exists only between them, and no single compiler invocation can see more than one.

5. Which Exceptions Actually Apply

The exceptions in §2's third part are not interchangeable, and choosing the wrong one produces §4's failure. Measured, on a header included by two translation units:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// vars.h
static int staticCounter = 0;     // ONE PER TU
inline int inlineCounter = 0;     // ONE PROGRAM-WIDE (C++17)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  TU A and TU B both include the header. Do they see one object or two?
    staticCounter: &A == &B ? TWO objects
    inlineCounter: &A == &B ? SAME object
  start            A: static=0 inline=0   B: static=0 inline=0
  after bumpFromA  A: static=1 inline=1   B: static=0 inline=1
  after bumpFromB  A: static=2 inline=2   B: static=0 inline=2

The program compares the two translation units' addresses rather than printing them, because the absolute values are randomised by ASLR on every run and would not reproduce — the relationship is the stable fact.

inlineCounter is one object, and its count rises 0, 1, 2 across both translation units. That is one variable, shared, which is what you almost always want from a header.

staticCounter is two objects. static at namespace scope means internal linkage — the name is not visible outside its translation unit — so each translation unit gets its own. That alone is not an ODR violation; it is two distinct variables that happen to share a spelling. The third line is where it becomes one, and §6 is why.

The object file says the same thing. nm -m a4.o, with the section offsets elided because they move with any edit:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
… (__DATA,__data) weak external _inlineCounter
… (__DATA,__bss) non-external __ZL13staticCounter

weak external is how the toolchain implements the exception: every translation unit emits a definition, they are all marked weak, and the linker keeps one and discards the rest. non-external means the symbol is invisible to the linker, and the _ZL prefix in the mangled name encodes the internal linkage — Chapter 24.5 §1 explains that encoding.

So the table for a header is:

in a header, writeto getbecause
inline functionone definition, sharedthe ODR exception, weak symbols
inline variable (C++17)one object, sharedsame mechanism
constexpr functionone definition, sharedimplicitly inline
constexpr variableone per TU, usually harmlessimplicitly const, internal linkage
a class or enum definitionone per TU, must be identicalthe ODR exception, unchecked
a templateinstantiated per TU, deduplicatedthe ODR exception
static variableone per TU — rarely what you wantinternal linkage
a plain function definitiona duplicate-symbol error§3
a plain variable definitiona duplicate-symbol error§3

Chapter 24.2 is that table with a reason for each row.

6. The Bug That Combines Them

static in a header is not a violation by itself. Combined with an inline function that uses it, it produces a program that is hard to explain from the source:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// vars.h
static int staticCounter = 0;
inline int bumpS() { return ++staticCounter; }   // inline fn + TU-local static

Two translation units include that, and only one of them calls bumpS():

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  start:            A=0 B=0
  after bumpViaB(): A=1 B=0   <-- which one moved?
  after bumpViaA(): A=2 B=0

Calling B's path incremented A's counter. B's counter never moves at all. The symbol table explains it exactly — nm -m on the executable finds one bumpS and two staticCounter objects:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
… T __Z5bumpSv
0000000100008004 (__DATA,__bss) non-external __ZL13staticCounter
0000000100008008 (__DATA,__bss) non-external __ZL13staticCounter

The two staticCounter entries are four bytes apart — two distinct objects in __bss — and there is exactly one __Z5bumpSv.

Two counters, one function. bumpS is inline, so both translation units emitted a weak definition and the linker kept one — and the one it kept was compiled against its own translation unit's staticCounter. Every call in the program, from either translation unit, now touches that one object. The other counter is live, addressable, printed by its own translation unit, and permanently zero.

This is a genuine ODR violation: the two bumpS definitions are token-identical but the name staticCounter inside them refers to different entities, which §2's third part forbids. It is undefined behaviour, no diagnostic is required, and none is issued. The fix is to make the variable inline rather than static, so that there is one of it to refer to.

Three verifications were needed to be sure of that explanation rather than merely plausible about it: the runtime counts, the symbol counts in the executable, and a directed test that exercises only B's path. The first alone would have supported several wrong stories.

7. What Gets Deduplicated, and How

It is worth knowing the mechanism, because it explains the error messages you will see.

Inline functions and templates are emitted as weak symbols in every translation unit that needs them, and the linker keeps one. The cost is compile time and object size, not program size — each of 50 translation units compiles the same inline function, and 49 copies are thrown away. Chapter 24.2 §3 measures that cost.

Class and enum definitions emit nothing. They are consumed entirely at compile time, which is why §4's violation is undetectable.

Static data members declared in a class need a definition somewhere — historically in exactly one .cpp, and since C++17 inline static in the class, which is simpler and the modern default.

Function-local static variables are one per function, not one per translation unit, and are initialised on first use with a thread-safe guard. That is why the function-local-static singleton works where a header-level static does not:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
inline std::vector<std::string>& registry()
{
    static std::vector<std::string> r;   // one, program-wide, because the
    return r;                            // inline FUNCTION is deduplicated
}

Chapter 24.6 §5 uses exactly this, and measures a different thing going wrong around it.

8. The Failures, With Their Causes

symptomcausehow it is found
duplicate symbol 'f()'a function or variable defined in a header without inline, or two .cpp files defining itthe linker names it and both files
Undefined symbols: f(int)a declaration with no matching definition; often a signature driftChapter 24.5 §3
Undefined symbols: f<T>(...)a template defined in a .cppChapter 24.2 §2
fields read at the wrong offsets, no diagnostictwo different definitions of one class§4 — nothing finds it
a counter that never incrementsstatic in a header used by an inline function§6 — nothing finds it
a value initialised in one place and zero in anotherstatic in a header, two objects§5
a global constructor that never runsan archive member nobody referencedChapter 24.6 §5

The two rows with "nothing finds it" are the ones worth internalising, and both are layout-or-identity disagreements between translation units rather than name conflicts.

9. What to Actually Do

  • Define every type in exactly one header, and have every consumer include it. This makes §4's violation unrepresentable rather than merely unlikely.
  • Mark header-level functions inline and header-level variables inline (C++17). Not static.
  • Never put a static variable at namespace scope in a header. Measured: one object per translation unit, and combined with an inline function, a counter that silently never moves.
  • Track header dependencies in the build. Chapter 24.8 §3 measures the stale-object version of §4 directly.
  • Treat a layout-affecting #ifdef as hostile. A macro that adds a member, defined for some targets, is §4 by construction.
  • Put a class's static data in inline static members, not in a .cpp-side definition.
  • Use a function-local static inside an inline function for a shared singleton, which is deduplicated correctly.
  • Read a duplicate symbol error as good news. It is the rule catching something before it ran.

10. Mistakes Worth Making Once

A function defined in a header without inline. The duplicate-symbol error, which at least stops the build.

int verbose = 0; in a header. That is a definition. inline int verbose = 0; is what you meant.

static in a header because it "makes the warning go away". It does, by creating one object per translation unit.

static plus an inline function. Measured: A=1 B=0 after calling only B's path.

Rebuilding some objects after a header change. §4's experiment, run by your build system on your behalf.

A debug-only member inside #ifdef. Half your targets have a different layout.

Assuming -Weverything would have caught it. Measured: 0 diagnostics.

Reading duplicate symbol as a build-system problem. It is usually a missing inline.

11. Interview Relevance

The frozen curriculum marks this chapter Critical, and it is one of the most reliable interview topics in the language because the answer separates people who have debugged a build from people who have read about one.

The question is usually "what is the one-definition rule?". A complete answer states all three parts — one definition per program, at most one per translation unit, and the list of entities exempted on condition of being token-identical — and then immediately distinguishes the two failures: a duplicate symbol is a link error naming the entity, and an inconsistent definition is undefined behaviour with no diagnostic required by the standard.

The follow-up that distinguishes a strong answer is why the second one is undetectable. Saying that a class definition emits no symbol — that offsets are baked into instructions as constants before the linker runs, so there is nothing left to compare — shows you understand the model rather than the rule.

If you can add the static-in-a-header measurement, that is a concrete story most candidates do not have: two counters, one inline function, and a variable that is never incremented by the translation unit that appears to own it.

12. What You Should Be Able to Say Now

  • That a translation unit is one source file after preprocessing, that it is compiled in complete ignorance of the others, and that the linker joins the results by name.
  • All three parts of the ODR, including that the identical-definition condition is unchecked and its violation needs no diagnostic.
  • That a duplicate symbol is a link error naming the entity and both object files, and that this is the friendly case.
  • That two different definitions of one class link cleanly and produce wrong offsets — measured, master read as -286331154, with 0 diagnostics from -Weverything and the linker.
  • Why: a class emits no symbol, and the offsets are constants inside instructions by the time the linker runs.
  • That inline in a header gives one shared entity (weak external) and static gives one per translation unit (non-external, _ZL-mangled).
  • The combination bug: one surviving inline function bound to one translation unit's static, so the other's is never touched — verified by runtime counts, symbol counts and a directed test.
  • That a function-local static in an inline function is the correct shared singleton.

13. Exercises

Exercise 1 — the loud one. Define the same function in two .cpp files and link them. Read the error. Then add inline to both and confirm it links, and check with nm -m that the symbols became weak.

Exercise 2 — the silent one. Reproduce §4. Then move the inserted field to the end of the struct and observe which values are now correct — and explain why that version is more dangerous.

Exercise 3 — the diagnostics. Run §4's program under -Wall -Wextra -Wpedantic, -Weverything, -fsanitize=address and -fsanitize=undefined. Record what each reports.

Exercise 4 — the combination. Reproduce §6. Count the bumpS and staticCounter symbols in the executable with nm -m. Then change static to inline and confirm both counters become one.

Exercise 5 — the table. For each row of §5's table, write the header, include it from two translation units, and verify the claim by printing the address of the entity from both.

14. Summary

  • A translation unit is one source file after preprocessing, compiled in ignorance of every other, and joined afterwards by a linker that matches names.
  • The ODR has three parts: exactly one definition per program; at most one per translation unit; and a list of entities — classes, enums, inline functions and variables, templates, constexpr functions — that may be defined in many translation units provided every definition is token-identical and its names mean the same things.
  • That condition is unchecked, and the standard requires no diagnostic for violating it.
  • The loud failure: duplicate symbol 'shared()', with both object files named. The build stops. Usually a missing inline.
  • The silent failure: two different definitions of one class compile, link and run. Measured, a field A wrote as master=2 was read by B as resp=2, and B's master read -286331154, with sizeof 8 against 12 and zero diagnostics from -Weverything or the linker.
  • The asymmetry is structural. Functions and variables have names in the object file, so conflicts are detectable. A class emits no symbol — offsets become integer constants inside instructions — so the information needed to detect the disagreement is gone before the linker starts.
  • So prevent it rather than detect it: one header per type, every consumer including it, and header dependencies tracked by the build.
  • In a header use inline, not static. inline gives one shared entity (weak external); static gives one per translation unit (non-external). Combined with an inline function, static produces a measured bug in which two counters exist, one function survives, and B's counter is never incremented — A=1 B=0 after calling only B's path.

Chapter 24.2 turns this rule into a decision for every kind of entity you can put in a header, with a reason for each rather than a convention.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.