ODR, Linkage, and Translation Units
One Definition Rule, internal vs external linkage, inline variables, static, anonymous namespaces, and header-only patterns in C++.
One Definition Rule (ODR)since C++98Every variable, function, class, enum, and template in a program must be defined exactly once; classes, inline functions, and templates may appear in multiple translation units only if all definitions are token-for-token identical and every referenced name resolves to the same entity.
Overview
A translation unit is a single .cpp file after the preprocessor runs β after #include expansion, macro substitution, and conditional compilation. Each TU compiles independently into an object file; the linker resolves external references and merges everything into an executable or library.
Linkage determines the scope at which a name is visible:
- External linkage: the name resolves to the same entity across all TUs. Default for functions and non-
constvariables at namespace scope. - Internal linkage: the name is confined to the current TU; the linker never sees it. Default for
constandconstexpr(since C++11) variables at namespace scope, and everything inside an anonymous namespace. - No linkage: local variables and local types β not visible outside their block.
The ODR interacts with linkage directly: entities with external linkage must have exactly one definition across the entire program. Entities with internal linkage may each carry their own definition per TU without conflict, since the linker treats them as unrelated symbols.
Linkage in Detail
External linkage
// api.h
void process(int x); // declaration β external linkage
extern double threshold; // declaration β external linkage
// api.cpp
void process(int x) { /* ... */ } // definition
double threshold = 0.01; // definitionNon-const variables and functions at namespace scope have external linkage by default. extern on a variable without an initializer is a declaration; with an initializer it is a definition.
Internal linkage
// In any .cpp file
static int call_count = 0; // C++98: internal linkage
static void log_call() { ++call_count; }
namespace { // C++98: anonymous namespace β preferred
int state = 0;
struct Worker { void run(); }; // the type also gets internal linkage
}const and constexpr variables at namespace scope default to internal linkage in C++ β the opposite of C:
const int max_retries = 3; // internal linkage β safe to put in a header
constexpr int buffer_sz = 64; // C++11: also internal linkageTo give a const variable external linkage:
// constants.h
extern const int protocol_version; // declaration
// constants.cpp
extern const int protocol_version = 2; // definition β extern required here tooThe One Definition Rule
The ODR has two distinct faces, each with different failure modes.
Face 1 β across the whole program: Every entity with external linkage that is used must have exactly one definition somewhere in the program. Violating this is undefined behavior; the linker may catch it (duplicate symbol error), but is not required to.
// file1.cpp
int shared = 42;
// file2.cpp
int shared = 99; // ODR violation β UB; linker usually rejects, but not guaranteedFace 2 β identical definitions across TUs: Classes, inline functions, and templates may appear in multiple TUs (via a shared header), but all definitions must be token-for-token identical, and every name referenced inside the definition must resolve to the same entity. Violation here is silent UB β no diagnostic is required, ever.
// file1.cpp compiled with -DSCALE=2
// file2.cpp compiled with -DSCALE=4
// shared.h
inline int scaled(int x) { return x * SCALE; }
// Two non-identical definitions β ODR violated.
// Linker picks one arbitrarily; the other TU silently gets wrong behavior.A subtler form involves using directives pulling in different overloads across TUs:
// header_a.h
namespace Impl { int transform(int); }
// header_b.h
namespace Impl { double transform(double); }
// shared.h β included after different headers in different TUs
inline auto run(int x) { return Impl::transform(x); }
// Each TU resolves Impl::transform to a different overload β ODR violationinline β Multiple Definitions, One Entity
inline instructs the linker that multiple identical definitions of the same entity may exist across TUs; they should be merged into one. It does not guarantee inlining at the call site.
// utils.h β safe to include from arbitrarily many TUs
inline int clamp(int v, int lo, int hi) { // C++98
return v < lo ? lo : v > hi ? hi : v;
}
inline constexpr double pi = 3.14159265358979; // C++17: inline variableWithout inline, a function definition in a header creates one definition per including TU β the linker will reject it as a duplicate symbol. With inline, each TU emits the definition into a COMDAT section and the linker discards all but one.
constexpr functions are implicitly inline since C++11. Function templates are implicitly inline as well.
template<typename T>
T square(T x) { return x * x; } // implicitly inline β safe in headersinline static data members (C++17)
Before C++17, a static data member needed an out-of-class definition in exactly one .cpp file:
// C++11/14 pattern
struct Config {
static const int max_conn = 100; // in-class initializer (integral only)
};
const int Config::max_conn; // out-of-class definition β required in one .cppC++17 eliminates the split with inline static:
// C++17 β entire definition lives in the header
struct Config {
inline static int max_conn = 100;
inline static const std::string version = "2.0";
};Anonymous Namespaces vs static
Both give internal linkage. Prefer anonymous namespaces in C++ for three reasons:
staticcannot be applied to types or function templates at namespace scope β anonymous namespaces can.- A namespace block visually groups all TU-private entities together, making intent clear.
- Types in anonymous namespaces cannot leak through template instantiations into other TUs.
// mymodule.cpp
namespace {
template<typename T> // static cannot qualify a template at namespace scope
T transform(T x) { return x * 2; }
struct State {
int value{0};
void advance() { ++value; }
};
State g_state;
}
void public_tick() { g_state.advance(); }Header-Only Libraries
inline functions, inline constexpr variables (C++17), and templates compose naturally into fully self-contained headers:
// geometry.h
#pragma once
#include <cmath>
inline constexpr double tau = 6.28318530717958647692; // C++17
template<typename T>
struct Vec2 {
T x{}, y{};
[[nodiscard]] T length() const { // [[nodiscard]] since C++17
return static_cast<T>(std::sqrt(x*x + y*y));
}
[[nodiscard]] Vec2 normalized() const {
T len = length();
return {x / len, y / len};
}
Vec2& operator+=(const Vec2& o) { x += o.x; y += o.y; return *this; }
};
// Free function β must be inline (not implicitly, unlike class template members)
inline double deg_to_rad(double deg) {
return deg * tau / 360.0;
}Every function defined inside a class template body is implicitly inline. Explicit inline is only needed for non-template free functions.
extern "C" β C Linkage
C++ mangles names to encode type signatures; C does not. extern "C" suppresses mangling and enforces C calling conventions:
// engine.h β usable from both C and C++
#ifdef __cplusplus
extern "C" {
#endif
void engine_init(int flags);
int engine_process(const char* data, int len);
void engine_shutdown(void);
#ifdef __cplusplus
}
#endif// engine.cpp β full C++ implementation behind a C-compatible API
extern "C" void engine_init(int flags) {
registry = std::make_unique<Registry>(flags); // C++ inside
}extern "C" functions have external linkage with C name conventions. Overloading is not possible: C has no overloading, and the mangled names that distinguish overloads are suppressed.
Common Pitfalls
Non-inline definitions in headers. A function body in a header without inline compiles cleanly per TU but fails at link time the moment two TUs include it. The error message ("multiple definition of...") points to the header, which surprises newcomers.
Macro-dependent inline bodies. If #define values differ between TUs (e.g. via differing compile flags), any inline function that uses those macros silently violates ODR. Replace macros with inline constexpr constants.
Mismatched struct definitions. Defining the same struct with different member types or ordering in two TUs β often via #ifdef β is silent UB. Layout mismatches produce no linker error; the program simply reads the wrong bytes.
const vs int in headers. const int x = 5; in a header is safe (internal linkage per TU). int x = 5; in a header is an ODR violation. Developers coming from C expect the opposite.
Missing out-of-class static member definition (pre-C++17). Omitting the .cpp-side definition produces a link error; accidentally putting it in a header (without inline) produces a silent ODR violation if multiple TUs include that header.
Best Practices
- Prefer anonymous namespaces over
staticfor TU-private code β they handle types and templates cleanly. - Every non-template free function defined in a header must be marked
inline. - Use
inline constexprfor constants shared across TUs (C++17); useextern const+ single-TU definition for C++11/14 targets. - Replace
#defineconstants used inside inline functions withinline constexprto avoid macro-dependent ODR violations. - Use
inline staticfor class-level state in headers (C++17+). - Enable LTO with
-Wodr(GCC) or ASAN+LTO to surface silent ODR violations during development β they are invisible at compile time by design.
Summary
| Construct | Linkage | Safe in header? | Notes |
|---|---|---|---|
namespace { ... } | Internal | Yes (per TU) | Preferred for TU-private code |
static (namespace scope) | Internal | Yes (per TU) | Cannot apply to types/templates |
const / constexpr variable | Internal | Yes | Default in C++, unlike C |
extern const variable | External | Declaration only | Definition in exactly one .cpp |
| Regular function / variable | External | Declaration only | Definition in exactly one .cpp |
inline function | External | Yes | All definitions must be identical |
inline constexpr variable | External | Yes | C++17 |
inline static member | External | Yes | C++17 |
| Template (function/variable) | External (implicit inline) | Yes | All definitions must be identical |
extern "C" | External (C linkage) | Declaration only | No overloading permitted |
Internal vs external linkage
Linkage is a property of a name, not of a type or value. The same entity can be declared with different linkage in different contexts, and getting that wrong produces either a linker error or silent undefined behavior.
// C++11 β summarising the default rules
const int A = 1; // internal linkage (const at namespace scope)
extern const int B = 2; // external linkage (explicit extern overrides const default)
int C = 3; // external linkage (non-const variable)
static int D = 4; // internal linkage (static forces it)
void foo(); // external linkage (function declaration)
static void bar() {} // internal linkageLocal variables have no linkage β they are not visible to the linker at all. A local static variable has internal storage duration but still no linkage; the static keyword means something different inside a function body than at namespace scope.
Unnamed namespaces and translation units
Every name declared inside an unnamed namespace receives a unique, TU-private internal-linkage identity. The compiler synthesises a unique namespace name per TU, so namespace { int x; } in a.cpp and namespace { int x; } in b.cpp are completely unrelated variables β no conflict, no sharing.
// widget.cpp // C++98
namespace {
// Only widget.cpp can see these
constexpr int kMaxRetries = 5;
struct Cache {
std::unordered_map<int, std::string> data;
};
Cache g_cache; // one per TU; no linker symbol exposed
template<typename T> // static cannot qualify templates β namespace {} can
T clamp_to_range(T v, T lo, T hi) {
return v < lo ? lo : v > hi ? hi : v;
}
}
void Widget::process(int id) {
auto val = clamp_to_range(id, 0, kMaxRetries);
// ...
}Nested unnamed namespaces are valid but unusual β a single anonymous block at the top of the TU is the idiomatic pattern. Avoid placing unnamed namespaces inside headers: every including TU gets its own independent copy of those entities, which silently inflates binary size.
inline variables and ODR (C++17)
inline on a variable tells the linker the same rule it already knew for inline functions: keep exactly one copy and discard the rest. Without inline, a variable definition in a header creates a separate object per TU; with it, COMDAT folding reduces them to one.
// config.h β C++17
#pragma once
inline constexpr int kVersion = 3; // one address across the program
inline constexpr double kEpsilon = 1e-9;
// Works for non-trivial types too
#include <string_view>
inline constexpr std::string_view kBanner = "bigcpp v3"; // C++17// Pre-C++17 equivalent β requires a paired .cpp file
// config.h
extern const int kVersion; // declaration
// config.cpp
const int kVersion = 3; // exactly one definitioninline static in a class follows the same rule β the variable lives in the class definition (in a header), and the linker merges all copies down to one.
// C++17 β header-only counter
struct Registry {
inline static int instance_count = 0; // C++17
Registry() { ++instance_count; }
~Registry() { --instance_count; }
};extern template to control instantiation
extern template suppresses implicit instantiation in the current TU, saving compile time and object-file bloat when the same template is used across many files. The linker relies on exactly one TU providing the explicit instantiation.
// vector_ops.h β C++11
template<typename T>
void sort_and_deduplicate(std::vector<T>& v);
// Suppress implicit instantiation in every including TU for these two types
extern template void sort_and_deduplicate<int>(std::vector<int>&); // C++11
extern template void sort_and_deduplicate<std::string>(std::vector<std::string>&); // C++11// vector_ops.cpp β the single TU that provides both instantiations
#include "vector_ops.h"
#include <algorithm>
template<typename T>
void sort_and_deduplicate(std::vector<T>& v) {
std::sort(v.begin(), v.end());
v.erase(std::unique(v.begin(), v.end()), v.end());
}
// Explicit instantiations β linker finds these; no other TU emits them // C++11
template void sort_and_deduplicate<int>(std::vector<int>&);
template void sort_and_deduplicate<std::string>(std::vector<std::string>&);Omitting the explicit instantiation in the .cpp while keeping extern template in the header produces a linker error ("undefined reference"). That failure is intentional and loud β far better than the silent ODR violation you would get without the extern suppression.
ODR violations detectable by sanitizers
AddressSanitizer combined with LTO can surface a subset of ODR violations at runtime, but many violations remain completely invisible β the program just runs with wrong behaviour. The -fsanitize=address flag alone is not enough; you need -flto -fsanitize=address -Wodr (GCC) or Clang's -flto -fsanitize=address.
// Demonstrates the class of violation that sanitizers can catch
// lib_a.cpp β compiled with -DUSE_DOUBLE
struct Param { double value; }; // 8 bytes
void consume(Param p); // defined here
// lib_b.cpp β compiled without -DUSE_DOUBLE
struct Param { int value; }; // 4 bytes β different layout, same name
void produce() {
Param p{42};
consume(p); // UB: consume reads 8 bytes, produce wrote 4
}GCC flags to add in a debug build:
# GCC β enable ODR warnings under LTO
g++ -O2 -flto -fsanitize=address -Wodr -fno-omit-frame-pointer \
lib_a.cpp lib_b.cpp -o programClang's -fsanitize=address,undefined with -flto=thin detects a broader class of violations. Neither catches the preprocessor-macro form (-DSCALE=2 vs -DSCALE=4 changing an inline function body) because the definitions look identical to the compiler β the macro has already been substituted.
LTO and ODR
Link-Time Optimisation gives the toolchain visibility across translation-unit boundaries, which both enables more ODR checking and can silently paper over violations in ways that confuse debuggers. With LTO, the linker holds IR for every TU and can verify that merged COMDAT definitions are truly identical.
# GCC thin-LTO build β ODR checking is stricter
g++ -O2 -flto=thin -Wodr source_a.cpp source_b.cpp -o app # C++11+
# Clang equivalent
clang++ -O2 -flto=thin source_a.cpp source_b.cpp -o appUnder LTO, the linker may fold two inline functions that appear identical into one even when they are technically different entities. If an ODR violation exists, this folding silently produces the wrong result with no diagnostic. Conversely, -Wodr under GCC LTO will warn when two type definitions differ in layout β a cheap safety net worth enabling in CI.
// Safe pattern: mark TU-private helpers to prevent accidental cross-TU merging
namespace { // C++98 β internal linkage, never merged
void internal_helper() { /* ... */ }
}Using [[gnu::visibility("hidden")]] (GCC/Clang) on external-linkage symbols that should not escape a shared library serves a similar purpose: the linker cannot merge what it cannot see.
See Also
- Modules β C++20 replaces textual inclusion with a partition model that eliminates most ODR exposure by design
- constexpr β
constexprfunctions are implicitlyinlinesince C++11;inline constexprvariables since C++17 - Templates β explicit specialization linkage and how template instantiations interact with the ODR