C++ One Definition Rule : Multiple Definitions and inline
Key takeaways
The ODR requires a single definition across the program for variables and functions, with exceptions for inline, templates, and C++17 inline variables. Avoid multiple definition link errors.
Introduction
The One Definition Rule (ODR) states that variables, functions, classes, etc. must have one definition in the program (with well-known exceptions). Understanding ODR prevents link errors and subtle bugs.
The rule exists because of how C++ is built. Each .cpp file is compiled separately into an object file, and the compiler never sees the other files. Headers are pasted into every file that includes them. The linker then combines the object files and must decide, for every name, which piece of code or storage it refers to. If two object files both provide a definition of int globalVar, the linker cannot know which one you meant. The ODR is the contract that makes that combination well defined.
It is useful to think of the ODR as having two halves with very different failure modes:
- Things that must be defined exactly once in the whole program: non-inline functions and non-inline variables with external linkage. Violations are usually caught by the linker with a clear error.
- Things that may be defined in several translation units, but must be identical everywhere: classes, enums, inline functions, inline variables and templates. Violations here are “ill-formed, no diagnostic required”: the program may compile, link and run while quietly using the wrong definition.
The first half causes annoying build errors; the second half causes the truly expensive bugs. Most of this article is about telling the two apart.
Basic ODR rules
Variables
// ❌ ODR violation
// file1.cpp
int globalVar = 10;
// file2.cpp
int globalVar = 20; // multiple definitions
// ✅ Correct pattern
// header.h
extern int globalVar; // declaration
// file1.cpp
int globalVar = 10; // single definition
// file2.cpp
#include "header.h" // declaration only
The broken version produces a link error such as multiple definition of 'globalVar' (GNU ld and LLD) or LNK2005: "int globalVar" already defined in file1.obj (MSVC). The fix separates the declaration (extern int globalVar;, which says “this exists somewhere” and creates no storage) from the definition (int globalVar = 10;, which creates the storage), and puts the definition in exactly one .cpp. file1.cpp should also include header.h. It is not required for linking, but it lets the compiler check that the definition matches the declaration; without it, changing the declaration to extern long globalVar; would still link, with each file silently using a different type for the same memory.
The opposite mistake, declaring with extern everywhere and defining nowhere, gives undefined reference to 'globalVar'. Note that the ODR is about external linkage. static int counter = 0; or a variable in an anonymous namespace at file scope has internal linkage, so each translation unit gets its own private variable and no conflict arises, which is a legitimate choice for file-local state and a source of confusion when used in a header by mistake.
Functions
// ❌ Two definitions of the same function
// file1.cpp
void func() { std::cout << "file1\n"; }
// file2.cpp
void func() { std::cout << "file2\n"; }
// ✅ Declaration in header, single definition in one .cpp
// header.h
void func();
// file1.cpp
void func() { std::cout << "implementation\n"; }
Two definitions of func in two .cpp files are always an error, even if the bodies are identical, because a non-inline function is supposed to have one address and one body. The linker reports it as a duplicate symbol. One subtlety is that functions are identified by their full signature, so void func(int) and void func(long) are different functions (overloads) and can each be defined once. That cuts the other way too: if the header declares void func(int) and the .cpp defines void func(long) by mistake, there is no ODR violation, just a missing definition, and the error shows up as undefined reference to 'func(int)' in whichever file calls it.
Static libraries add a twist. When the duplicate definitions live in two different .a or .lib archives, the linker often takes the first one it needs and never looks at the second, so the program links without complaint. Which definition wins then depends on link order, which is exactly the kind of silent ODR violation described below.
ODR exceptions
inline functions
// utils.h
inline int add(int a, int b) {
return a + b;
}
// file1.cpp
#include "utils.h"
// file2.cpp
#include "utils.h" // OK if identical
Key point: inline definitions may appear in multiple TUs if token-identical.
In modern C++, inline has very little to do with inlining. Its real meaning is “this definition may appear in more than one translation unit; the linker should keep one”. The compiler emits the function in every object file that uses it, marked as a weak or COMDAT symbol, and the linker discards all but one copy. Whether calls are actually inlined is a separate optimization decision the compiler makes regardless of the keyword. Member functions defined inside a class body, constexpr functions and function templates are implicitly inline.
“Identical” means more than the same text. The definitions must consist of the same tokens and those tokens must mean the same thing in each translation unit. If an inline function uses a macro, a type alias or a constant that is defined differently in two .cpp files, the definitions differ even though the header text is the same. Because the linker keeps only one copy, both files end up running whichever version it picked.
Templates
Templates are typically defined in headers and instantiated in multiple TUs—ODR allows this under the usual rules for templates.
When two files both use std::vector<int>, each object file contains its own instantiation of the member functions it used, and the linker merges them just like inline functions. The same identical-definition requirement applies, and it can be violated in less obvious ways. A classic example is an explicit specialization visible in only some files: if a.cpp sees template<> struct Hash<MyKey> { ... } and b.cpp does not, b.cpp instantiates the primary template instead, and the two files disagree about what Hash<MyKey> is.
constexpr / inline variables (C++17)
inline constexpr int MAX = 100; // single definition across TUs (C++17 inline variable)
Before C++17 there was no way to define a variable in a header and have all translation units share one object; you needed extern in the header plus one definition in a .cpp. inline variables fix that: inline int counter = 0; in a header gives the program one counter, with one address. For constants, the older constexpr int MAX = 100; at namespace scope also works in a header, but for a different reason: namespace-scope const and constexpr variables have internal linkage, so every translation unit gets its own copy. That is harmless for plain integers, but it means &MAX differs between files, and a large constant array defined that way is duplicated in every object file. inline constexpr gives a single shared object and is the idiomatic choice for header constants since C++17.
Practical examples
Example 1: Global configuration
// config.h
#ifndef CONFIG_H
#define CONFIG_H
extern int maxConnections;
extern const char* appName;
inline int maxRetries = 3;
#endif
// config.cpp
#include "config.h"
int maxConnections = 100;
const char* appName = "MyApp";
The header mixes the two strategies on purpose. maxConnections and appName use the classic split, so their initial values live in config.cpp and changing them recompiles one file. maxRetries is a C++17 inline variable, so its value is in the header and every file that includes config.h shares one object. Both are correct; the trade-off is recompilation cost versus convenience. A detail worth noticing: const char* appName is a non-const pointer to const characters, so it has external linkage and needs the extern pattern. If it were const char* const appName = "MyApp"; (a const pointer), it would have internal linkage and could be defined directly in the header, one copy per translation unit.
Mutable globals like these are also subject to the static initialization order problem: if another global in a different .cpp reads maxConnections during its own initialization, it may run before config.cpp has initialized it. Constant-initialized values such as int x = 100; are set before any dynamic initialization, so they are safe; values computed by function calls at startup are not.
Example 2: Class in header, members in .cpp
Class definitions in headers must be identical in every TU that sees them—normally one shared header.
// point.h
#pragma once
struct Point {
int x = 0, y = 0;
int manhattan() const { return (x < 0 ? -x : x) + (y < 0 ? -y : y); } // implicitly inline
double length() const; // defined in point.cpp
};
// point.cpp
#include "point.h"
#include <cmath>
double Point::length() const { return std::sqrt(double(x) * x + double(y) * y); }
The class definition itself appears in every file that includes point.h, which the ODR allows because each copy is identical. manhattan is defined inside the class, so it is implicitly inline and may also appear everywhere. length is declared in the class but defined once in point.cpp, like any ordinary function. If someone later copies length’s body into the header without inline and outside the class, it becomes a non-inline definition in every translation unit and the linker reports duplicates.
Example 3: static data members
Pre-C++17: define non-inline static data members in one .cpp. C++17 allows inline static members in-class.
// counter.h
#pragma once
struct Counter {
static int created; // declaration only (pre-C++17 style)
inline static int destroyed = 0; // C++17: definition in the class
static constexpr int kMax = 64; // implicitly inline since C++17
};
// counter.cpp
#include "counter.h"
int Counter::created = 0; // the one definition
A static data member declared in the class is only a declaration; forgetting the out-of-class definition gives undefined reference to 'Counter::created', one of the most common link errors for newcomers. static constexpr members are the historical trap. Before C++17, kMax had to be defined in a .cpp as well if it was odr-used, for example bound to a const int& by std::max(n, Counter::kMax) or std::vector::push_back(Counter::kMax). Code that only used it as a value compiled fine, then failed to link the day someone passed it by reference. Since C++17, static constexpr data members are implicitly inline and that problem is gone.
Common problems
Problem 1: Non-inline function definitions in headers
Put non-inline definitions in a single .cpp, or mark small functions inline in the header.
This one is confusing because it works until it does not. A header with int helper() { return 42; } builds fine while only one .cpp includes it; the day a second file includes it, the link fails with multiple definition of 'helper()', and the error message names two object files that were never edited. Include guards do not help, since they only prevent double inclusion within one translation unit.
Problem 2: Different definitions of the same type
If two TUs define struct Data differently, ODR is violated and behavior is undefined even if it “compiles.”
Fix: define shared types in one header included everywhere.
The most common way this happens is not two headers but two helpers with the same name. Two .cpp files each define a small local struct Node or class Impl in the global namespace, with different members. Each file compiles, the program links, and the implicitly inline member functions and constructors of the two classes are merged by the linker as if they were one. One file then runs the other file’s constructor on an object with a different layout, corrupting memory in a way that looks nothing like a naming problem. I have seen this show up as a crash inside std::vector growth, far from the real cause. Putting file-local types and helpers in an anonymous namespace gives them internal linkage and makes the collision impossible.
The other common source is inconsistent build settings: a header that changes a struct’s layout based on a macro (#ifdef DEBUG_FIELDS), or #pragma pack, compiled with the macro set in some files and not others. Mixing libraries built with different _ITERATOR_DEBUG_LEVEL settings on MSVC is the same problem at a larger scale; MSVC detects that particular case at link time with LNK2038: mismatch detected for '_ITERATOR_DEBUG_LEVEL'.
Problem 3: Anonymous namespaces in headers
Each TU gets its own anonymous namespace, so anything defined in an anonymous namespace inside a header becomes a separate copy per .cpp. Avoid it for anything that should share state (a registry, a cache, a counter), because each file would silently get its own. It is also a subtle ODR hazard when an inline function or template in the header refers to such an entity: the “same” inline function then refers to different objects in different translation units, so its definitions are not identical.
Problem 4: constexpr variables before C++17
Prefer inline constexpr in C++17+ for header-defined constants with a single-definition guarantee. Before C++17, namespace-scope constexpr variables were duplicated per translation unit (internal linkage), and static constexpr class members needed an out-of-class definition whenever they were odr-used, as described in Example 3.
ODR-friendly patterns
Header / source split
Declarations and inline/template in headers; non-inline function bodies in one .cpp.
Logging helpers as inline in headers
Short inline void log(...) in headers is a common pattern.
The rule of thumb that keeps most projects ODR-clean: headers contain declarations, types, and things that are inline by nature (templates, constexpr, small inline functions, inline variables); .cpp files contain everything else, and anything private to a .cpp goes in an anonymous namespace. Following that consistently eliminates both the duplicate-symbol link errors and the silent class-name collisions.
Library-style example
Singleton or logger patterns: declare static members appropriately; define inline static in-class when possible (C++17).
// logger.h
#pragma once
#include <mutex>
#include <string_view>
#include <iostream>
class Logger {
public:
static Logger& instance() {
static Logger inst; // one object program-wide: inline function + function-local static
return inst;
}
void log(std::string_view msg) {
std::lock_guard lock(mu_);
std::cerr << prefix << msg << '\n';
}
inline static std::string_view prefix = "[app] "; // C++17 inline static member
private:
Logger() = default;
std::mutex mu_;
};
instance() is defined in the class, so it is inline, and the function-local static Logger inst belongs to that one inline function, so the whole program shares one logger even though the header is included everywhere. Since C++11 its initialization is thread-safe. This “Meyers singleton” also sidesteps the static initialization order problem, because the object is created on first use rather than at program start.
The pattern has a known limitation with shared libraries. If the header-only singleton is compiled into two separate DLLs or .so files, each library may get its own copy of inst, because inline-function merging happens per linked binary. On Linux, symbol interposition often merges them anyway at load time; on Windows, each DLL keeps its own unless the function is exported. When a singleton must be unique across library boundaries, define instance() out of line in one library’s .cpp and export it.
Detecting violations
- Linker:
multiple definition of ...for duplicate strong symbols nm -C file.o | grep ' T 'across.ofiles to list defined functions and spot duplicates- GCC with LTO:
-flto -Wodrreports many mismatched type and inline-function definitions across translation units - AddressSanitizer: detects some ODR violations for global variables at startup (
detect_odr_violationis on by default), reportingERROR: AddressSanitizer: odr-violation - The gold linker:
--detect-odr-violationscompares debug information for inline functions
Ordinary warnings such as -Wall -Wextra do not help here, because each translation unit is fine on its own; only a tool that sees the whole program can notice that two of them disagree. That is why the silent half of the ODR survives code review so easily. In practice, turning on -Wodr in an LTO build and running tests under ASan catches most real cases, and consistent use of anonymous namespaces prevents the most common one.
What goes in the header and what in the source
| Item | Header | Source |
|---|---|---|
| Variable declaration | extern int var; | int var = 0; |
| Function declaration | void f(); | void f() {} |
| Class definition | class C {}; | — |
| inline function | inline void f() {} | — |
| Template | full definition in header | — |
The violations in this table produce linker errors, which are annoying but safe. The dangerous ODR violation is the one the linker does not report: two different definitions of a class or inline function with the same name in different source files. A helper struct Node defined differently in two .cpp files, or a header compiled with different macro settings in two libraries, links without complaint, and the program uses one layout in some places and the other elsewhere. Put file-local types and helpers in an unnamed namespace, and build with -flto -Wodr (GCC) or run AddressSanitizer with its ODR checks when mixing libraries.
Next: Extern linkage, Header files, Function overloading.
Related Articles
- C++ Compilation Process
- C++ multiple definition error
- C++ Linking
- C++ Name Mangling
- C++ extern Linkage — External vs Internal Linkage, extern
- C++ Function Overloading
- C++ Header Files
Frequently Asked Questions (FAQ)
Q. Can an ODR violation compile and link without any error?
A. Yes. If two translation units define the same inline function, class or template with different bodies, the program is ill-formed but no diagnostic is required, so the linker usually keeps one definition and the program silently runs code you did not expect. This often happens when two files define a helper class with the same name in the global namespace, or when a header is compiled with different macro settings. Putting file-local helpers in an anonymous namespace helps, and GCC can report some mismatches with -Wodr when building with LTO.