← All Posts
From Scratch · C++ » Project Structure

One Definition Rule (ODR)

The ODR Stated Clearly

The One Definition Rule is one of the most fundamental rules in C++, and one of the most violated. It bridges the gap between the language's compilation model (independent translation units) and the linker's requirement that every symbol resolve to exactly one definition. Violating the ODR is undefined behavior that the compiler is not required to diagnose. Your program might appear to work, then fail mysteriously on a different compiler, optimization level, or platform.

The ODR has two parts, each governing a different scope:

Part 1: Within a Single Translation Unit

Within a single TU, every class, function, variable, enumeration, and template must have at most one definition. Multiple definitions within the same TU are always a compile-time error:

// This is a compile error: two definitions in the same TU
void helper() { /* v1 */ }
void helper() { /* v2 */ }   // error: redefinition of 'helper'

class Widget { int x; };
class Widget { int y; };     // error: redefinition of 'Widget'

This part of the ODR is easy to enforce because the compiler processes one TU at a time and can detect duplicates directly.

Part 2: Across Translation Units

Across the entire program (all TUs linked together), certain entities may appear in multiple TUs, but their definitions must be identical in every TU. This applies to:

"Identical" is defined very precisely: the token sequences must be the same, and every name must refer to the same entity. If two TUs include the same header, and the header defines an inline function, both TUs get the same tokens, and the ODR is satisfied. But if the definitions differ in any way, you have an ODR violation.

ODR Violations: Why They Are Undefined Behavior

Here is the dangerous part: the C++ standard says that an ODR violation across TUs results in undefined behavior, no diagnostic required. This means:

The reason the standard does not require a diagnostic is that checking ODR compliance across TUs would require the linker to compare full token-by-token definitions of entities, which is impractical with the traditional compilation model (the definitions are in the source code, but the linker only sees compiled machine code).

A Classic ODR Violation

// config.h
#pragma once
#ifdef USE_DOUBLE
    using Real = double;
#else
    using Real = float;
#endif

// math.h
#pragma once
#include "config.h"
struct Point {
    Real x, y;
};
inline double distance(const Point& a, const Point& b) {
    return std::sqrt((a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y));
}

// a.cpp
#define USE_DOUBLE
#include "math.h"       // Point has {double x, y}: sizeof(Point) = 16
void func_a(Point p) { /* uses 16-byte Point */ }

// b.cpp
#include "math.h"       // Point has {float x, y}: sizeof(Point) = 8
void func_b() {
    Point p{1.0f, 2.0f};
    func_a(p);           // passes an 8-byte Point to a function expecting 16 bytes!
}

Both TUs include math.h, but the preprocessing context differs (USE_DOUBLE is defined in a.cpp but not in b.cpp). The Point struct and distance function have different definitions in the two TUs. The linker picks one definition of distance (arbitrarily) and discards the other. Whichever TU's code uses the "wrong" version will read member offsets incorrectly, leading to data corruption, crashes, or silently wrong results.

Key insight: ODR violations are insidious because they are silent. The code compiles and links without errors. The bug manifests as mysterious data corruption, crashes in unrelated code, or behavior that changes with optimization level or link order.

Inline Functions and the ODR

The inline keyword has evolved far beyond its original meaning of "please inline this call." In modern C++, the primary effect of inline is to grant a function permission to be defined in multiple TUs without violating the ODR, provided all definitions are identical.

// helper.h
#pragma once
inline int square(int x) {
    return x * x;
}
// This definition appears in every TU that includes helper.h.
// The compiler emits a copy in each .o file (as a weak/COMDAT symbol).
// The linker keeps one and discards the rest.
// As long as every TU sees the same definition, no ODR violation.

Without inline, defining a function in a header included by multiple TUs gives each TU a strong definition, and the linker reports "multiple definition of 'square'." With inline, the copies are emitted as weak symbols (or COMDAT groups), and the linker deduplicates them.

Inline Variables (C++17)

C++17 extended the inline mechanism to variables. An inline variable can be defined in a header and included in multiple TUs, with the linker keeping exactly one copy:

// constants.h
#pragma once
inline constexpr int MAX_SIZE = 1024;
inline std::string DEFAULT_NAME = "unnamed";   // one shared instance across all TUs

Before C++17, achieving a single shared global required extern declaration in the header with a definition in exactly one .cpp file. Inline variables eliminated this boilerplate.

Implicitly Inline: Member Functions Defined in Class

Member functions defined inside the class body are implicitly inline:

class Widget {
public:
    int value() const { return x_; }  // implicitly inline
private:
    int x_;
};

This is why you can put member function bodies in headers without linker errors. The implicit inline grants the same multi-TU permission. However, if you define a member function outside the class in a header without explicitly marking it inline, you get a "multiple definition" error:

// widget.h
class Widget {
public:
    int value() const;   // declaration only
};

// BAD: defined in header without inline, multiple definition errors!
int Widget::value() const { return x_; }

// GOOD: explicitly inline
inline int Widget::value() const { return x_; }

Header-Only Libraries and the ODR

Header-only libraries (like Catch2, nlohmann/json, stb_image) define all their code in headers. Every TU that includes them gets a full copy. This works because the code is structured to comply with ODR rules:

The ODR requirement for header-only libraries is that every TU must include the same version of the header. If one TU includes version 3.1 and another includes version 3.2 (perhaps from different include paths), and the definitions differ, you have an ODR violation.

// Pattern for global state in header-only libraries:
inline Config& global_config() {
    static Config instance;   // function-local static: one instance, thread-safe init
    return instance;
}
// Every TU that calls global_config() shares the same Config object.
Key insight: Header-only libraries are convenient but increase compile times (every TU reprocesses the entire library) and magnify the risk of ODR violations if different TUs see different versions or preprocessor contexts.

Templates and the ODR

Templates have a special relationship with the ODR. A template definition can appear in multiple TUs (it must appear in every TU that instantiates it), and each TU may instantiate the template for different types. The ODR requires that:

// math.h
#pragma once
template<typename T>
T square(T x) {
    return x * x;
}
// a.cpp includes math.h and uses square<int>: emits code for square<int>
// b.cpp includes math.h and uses square<int>: also emits code for square<int>
// The linker sees two identical weak copies and keeps one. ODR satisfied.

Where templates and ODR interact dangerously is when the template definition depends on something that differs between TUs:

// Dangerous: template behavior depends on a macro
#define PRECISION 4
#include "formatter.h"   // uses PRECISION in a template
// If another TU defines PRECISION as 8 and includes the same header,
// the template instantiations will differ: ODR violation.

Explicit Template Instantiation

You can control which TU emits a template instantiation using extern template and explicit instantiation:

// vector_math.h
#pragma once
template<typename T>
T dot_product(const T* a, const T* b, int n);

// Suppress implicit instantiation in every TU that includes this:
extern template float dot_product<float>(const float*, const float*, int);
extern template double dot_product<double>(const double*, const double*, int);

// vector_math.cpp
#include "vector_math.h"
template<typename T>
T dot_product(const T* a, const T* b, int n) {
    T sum = 0;
    for (int i = 0; i < n; ++i) sum += a[i] * b[i];
    return sum;
}
// Explicit instantiation: only this TU emits the code
template float dot_product<float>(const float*, const float*, int);
template double dot_product<double>(const double*, const double*, int);

This pattern eliminates redundant template instantiations across TUs, reducing compile and link times in large projects.

Anonymous Namespaces and the ODR

An anonymous namespace gives every entity within it internal linkage: the entities are unique to the TU and invisible to the linker. This is the modern replacement for static at namespace scope:

// a.cpp
namespace {
    int counter = 0;
    void helper() { counter++; }
}
// a.cpp's counter and helper are private to a.cpp

// b.cpp
namespace {
    int counter = 0;           // completely independent from a.cpp's counter
    void helper() { counter--; }
}
// b.cpp's counter and helper are private to b.cpp

From the linker's perspective, these are different symbols with mangled names that include a unique identifier for each TU. There is no conflict and no ODR issue.

Anonymous namespaces are the idiomatic C++ way to create TU-private entities. The static keyword at namespace scope is a C holdover that works the same way but is considered less expressive.

Anonymous Namespaces in Headers: A Subtle Trap

Putting an anonymous namespace in a header is almost always a mistake:

// BAD: anonymous namespace in a header
#pragma once
namespace {
    int helper_counter = 0;   // each TU gets its OWN copy
}
class Widget {
public:
    void increment() { helper_counter++; }  // each TU increments a DIFFERENT counter
};

Every TU that includes this header gets its own independent helper_counter. If you expected all widgets to share one counter, you have a bug. Worse, the inline member function increment() has different behavior in different TUs (each modifies a different counter), which is an ODR violation for the inline function.

Key insight: Anonymous namespaces are for .cpp files only. Putting them in headers creates per-TU copies of variables and can cause ODR violations for inline functions that reference them.

More ODR Violation Patterns

Different Compiler Flags

If one TU is compiled with a flag that changes the ABI or struct layout, and another TU is compiled without it, definitions that span both TUs may differ:

# TU a.cpp compiled with pack(1):
# struct Foo { char c; int x; };   sizeof = 5
# TU b.cpp compiled normally:
# struct Foo { char c; int x; };   sizeof = 8 (with padding)
# Both TUs "see" the same source code, but the compiled layout differs.
# If b.cpp passes a Foo to a function compiled in a.cpp, the offsets are wrong.

Different Include Orders Affecting Overload Sets

// logger.h
#pragma once
#include <string>
inline void log(const std::string& msg) { /* ... */ }

// extended_logger.h
#pragma once
#include "logger.h"
inline void log(int code) { /* ... */ }

// a.cpp
#include "extended_logger.h"
// log() overload set: {string, int}

// b.cpp
#include "logger.h"
// log() overload set: {string}
// If a template in a header calls log(42) and is instantiated in both TUs,
// TU a.cpp resolves it to log(int), TU b.cpp has no match or converts to string.
// Different instantiations for the same template: ODR violation.

Conditional Compilation in Headers

// feature.h
#pragma once
struct Config {
    int base_value;
#ifdef ENABLE_FEATURE_X
    int feature_x_data;     // exists in some TUs, not others
#endif
};
// If ENABLE_FEATURE_X is defined in some TUs but not others,
// sizeof(Config) differs, and any code touching Config is an ODR violation.

How to Diagnose ODR Violations

Because the compiler and linker often do not report ODR violations, you need specialized tools and techniques:

ASAN (AddressSanitizer) with ODR Detection

# Clang's ASAN includes ODR violation detection:
clang++ -fsanitize=address -fno-omit-frame-pointer -O1 a.cpp b.cpp -o app
./app
# Reports: "ODR violation: global 'Config' has different sizes in different modules"

Gold Linker with --detect-odr-violations

# The gold linker can detect some ODR violations:
g++ -fuse-ld=gold -Wl,--detect-odr-violations a.o b.o -o app
# Warning: "ODR violation: 'helper()' first defined in a.o, also defined in b.o"

MSVC /INCLUDE and Linker Warnings

MSVC's linker sometimes reports "LNK4006: symbol already defined" or "LNK2005: symbol already defined" for ODR violations that involve non-inline functions. Enable /W4 for maximum linker warnings.

Compilation Database and Static Analysis

Tools like clang-tidy with the misc-definitions-in-headers check can flag non-inline definitions in headers, which are a common source of ODR violations. The cppcheck tool also has some ODR-related checks.

Manual Techniques

// Defensive: detect layout mismatches at compile time
struct Config {
    int base_value;
    int feature_data;
};
static_assert(sizeof(Config) == 8, "Config layout mismatch: check compile flags");

▶ ODR Violation: How Different Definitions Cause Corruption

Step through an ODR violation: two TUs define a class differently, and the linker picks one layout, causing the other TU to use wrong offsets.

Writing ODR-Safe Code

Follow these practices to avoid ODR violations:

  1. Never use conditional compilation that changes type layouts or function bodies in headers. If you must, ensure every TU uses the same macro definitions.
  2. Mark all function definitions in headers as inline (or define them inside the class body, which is implicitly inline).
  3. Use inline variables (C++17) for global variables defined in headers.
  4. Keep anonymous namespaces out of headers.
  5. Use the same compiler flags across all TUs, especially flags that affect ABI: struct packing, RTTI, exception handling, and standard version.
  6. Use a build system (CMake, Meson, Bazel) that enforces consistent compilation settings across all targets.
  7. Enable ASAN and use the gold linker's ODR detection in your CI pipeline.
  8. Prefer static_assert on struct sizes in headers to catch layout mismatches early.

ODR and C++20 Modules

C++20 modules fundamentally change the equation. A module is compiled once and its compiled interface is shared with all importers. There is no textual inclusion, no macro pollution, and no duplicate processing. The module system can enforce ODR at the module boundary because the compiler (not just the linker) sees the complete definition.

// math.cppm (module interface)
export module math;
export struct Point {
    double x, y;
};
export double distance(const Point& a, const Point& b);

// Importers see one canonical definition: no ODR risk from include-order
// or macro-dependent differences.

Modules eliminate most categories of ODR violations by construction. We explore modules in detail in the Build Systems & CMake article.

Summary