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:
- Inline functions
- Inline variables (C++17)
- Class definitions
- Templates (function templates, class templates)
- Enumerations used by multiple TUs
"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 compiler will not warn you.
- The linker might not warn you (some linkers detect some violations, but it is not guaranteed).
- Your program might appear to work correctly in debug mode but crash in release mode.
- It might work on GCC but fail on MSVC.
- It might work today and break when you add a seemingly unrelated source file.
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.
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:
- All functions are
inline(either explicitly or implicitly via class member definitions). - All templates are inherently ODR-safe (discussed below).
- Global state, if any, uses
inlinevariables (C++17) or function-local statics.
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.
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:
- The template definition must be the same in every TU.
- Each instantiation of the template for a specific set of template arguments must produce the same code.
// 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.
.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
- Search for conditional compilation in headers: Any
#ifdefin a header that affects type definitions or function bodies is a potential ODR violation if TUs define the macro inconsistently. - Check for non-inline function definitions in headers:
grep -rnfor function bodies in.hfiles that do not have theinlinekeyword. - Ensure consistent compile flags: All TUs should be compiled with the same flags that affect ABI (optimization level variations are usually safe, but struct packing, RTTI, and exception flags must be consistent).
- Use
sizeofchecks: Add static assertions to headers to verify struct sizes match expectations.
// 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:
- Never use conditional compilation that changes type layouts or function bodies in headers. If you must, ensure every TU uses the same macro definitions.
- Mark all function definitions in headers as
inline(or define them inside the class body, which is implicitly inline). - Use
inlinevariables (C++17) for global variables defined in headers. - Keep anonymous namespaces out of headers.
- Use the same compiler flags across all TUs, especially flags that affect ABI: struct packing, RTTI, exception handling, and standard version.
- Use a build system (CMake, Meson, Bazel) that enforces consistent compilation settings across all targets.
- Enable ASAN and use the gold linker's ODR detection in your CI pipeline.
- Prefer
static_asserton 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
- The ODR requires exactly one definition per entity within a TU, and identical definitions across TUs for entities that appear in multiple TUs.
- ODR violations are undefined behavior, no diagnostic required. They cause silent data corruption, crashes, and non-reproducible bugs.
inlinegrants functions and variables permission to be defined in multiple TUs, provided all definitions are identical.- Templates are inherently multi-TU, but the template definition and all names it references must be the same in every TU.
- Anonymous namespaces provide internal linkage and should only be used in
.cppfiles, never in headers. - Conditional compilation in headers that changes type layouts or function bodies is the most common source of ODR violations.
- Use ASAN, gold linker's
--detect-odr-violations, and static assertions to catch violations. - C++20 modules eliminate most ODR risks by replacing textual inclusion with compiled interfaces.