Skip to content
C++
Language
Intermediate

Conflicting Declarations

Conflicting declarations arise when the same name is declared with incompatible types, linkage, or definitions, violating the One Definition Rule.

Conflicting Declarationsince C++98

A conflicting declaration occurs when the same name is redeclared in an incompatible way β€” differing type, linkage, or storage class β€” or when a name is defined more than once in violation of the One Definition Rule (ODR).

Overview

C++ draws a sharp distinction between a declaration and a definition. A declaration introduces a name into a scope; a definition is a declaration that also creates the entity it names. The language permits multiple compatible declarations of the same name, but conflicts arise in two distinct categories:

  1. Two declarations in the same scope disagree on the entity's type, linkage, or other properties.
  2. A name is defined more than once across the program β€” an ODR violation.

These categories differ in how they are diagnosed. Intra-scope type conflicts are hard compiler errors. Cross–translation-unit (cross-TU) ODR violations may or may not be reported by the linker; when undetected, they produce undefined behavior that can manifest as incorrect computation, memory corruption, or silent crashes.

Not every second declaration of the same name is a conflict. A declaration is permitted to reintroduce a name that was previously declared in the same scope, provided the two declarations are compatible:

cpp
extern int error_code;    // declaration
extern int error_code;    // OK: same type, same linkage β€” compatible redeclaration

void process(int n);      // declaration
void process(int n);      // OK: identical signature β€” compatible redeclaration

struct Node;              // forward declaration
struct Node { int val; }; // OK: definition follows forward declaration

Intra-Scope Type Conflicts

Declaring the same name twice in the same scope with incompatible types is an immediate compile error:

cpp
int value = 0;
double value = 3.14;  // error: conflicting declaration 'double value'
                      //        'value' previously declared as 'int'

Function overloads are not a special case here. Overload resolution discriminates on parameter types, not return types. Declaring two functions that differ only in return type is a conflict, not an overload:

cpp
int   compute(int x);    // declaration
float compute(int x);    // error: conflicting declaration β€” same parameter list, different return type

Class-type redefinitions are similarly rejected:

cpp
struct Config { int level; };
struct Config { int level; double threshold; }; // error: redefinition of 'struct Config'

Even if the two definitions are byte-for-byte identical, the compiler rejects a second class definition in the same TU. Include guards exist precisely to prevent a header from being processed twice, which would trigger this error.

The One Definition Rule

The ODR states that every entity used in a program shall be defined exactly once across all translation units. Functions and variables with external linkage may be declared in multiple TUs via a shared header, but the definition must appear in exactly one TU.

cpp
// widget.h
extern int widget_count; // declaration β€” legal in every TU that includes this header
void render_widget();    // declaration β€” ditto

// widget.cpp
int widget_count = 0;        // definition β€” exactly one TU
void render_widget() { /* */ } // definition β€” exactly one TU

Placing a non-inline function body in a header and including that header in two or more .cpp files creates duplicate symbols the linker will reject:

cpp
// utils.h β€” WRONG: non-inline function definition in a shared header
int clamp(int v, int lo, int hi) {
    return v < lo ? lo : v > hi ? hi : v;
}

// a.cpp β€” #include "utils.h" β†’ defines clamp
// b.cpp β€” #include "utils.h" β†’ also defines clamp
// Linker: error: multiple definition of 'clamp(int, int, int)'

ODR-Safe Exceptions

Several constructs are explicitly permitted to appear in multiple TUs, provided each definition is token-for-token identical and names the same entities:

  • inline functions (C++98) β€” the linker merges duplicate definitions and uses one.
  • constexpr functions (C++11) β€” constexpr implies inline for functions; safe in headers.
  • inline variables (C++17) β€” extends the same rule to object definitions.
  • constexpr variables at namespace scope (C++17) β€” implicitly inline in C++17; prior to C++17 they had internal linkage (via implied const), giving each TU its own copy.
  • Class definitions, template definitions, and template specializations β€” all subject to the identical-definition requirement.
cpp
// safe_utils.h β€” all definitions here are ODR-safe

inline int clamp(int v, int lo, int hi) { // C++98: inline, multiple TUs allowed
    return v < lo ? lo : v > hi ? hi : v;
}

constexpr int square(int n) { return n * n; } // C++11: constexpr implies inline for functions

inline constexpr double kPi = 3.14159265358979323846; // C++17: inline variable, single address

Subtle ODR Violations

The most dangerous ODR violations are those the linker cannot detect. The standard does not require a diagnostic for cross-TU ODR violations; it merely declares the program ill-formed with no required diagnostic (IFNDR). A class definition that silently diverges between TUs is the canonical example:

cpp
// packet.h
#ifdef EXPERIMENTAL
struct Packet { int id; double payload; float weight; };
#else
struct Packet { int id; double payload; };
#endif

// reader.cpp β€” compiled WITHOUT -DEXPERIMENTAL
// writer.cpp β€” compiled WITH    -DEXPERIMENTAL
// Both object files link successfully.
// The program has undefined behavior: layout mismatch between TUs.

The linker sees two Packet symbols matching in name only. Code in reader.cpp accesses payload at offsetof(Packet, payload) relative to its own layout; writer.cpp allocates a larger object. Reads and writes to weight from writer.cpp address memory that reader.cpp never allocated. The failure mode may be silent corruption, a segfault on an unrelated access, or apparently correct behavior that breaks under optimization.

A more mundane variant occurs when a stale object file is linked after a header change:

cpp
// After adding a member to Config, only some TUs are recompiled.
// The linker combines old and new object files without complaint.
// Accessing the new member from old code reads garbage.

Incremental build systems that track header dependencies prevent this, but manually invoked compilers or misconfigured build rules will not.

Best Practices

Declare in headers, define once. Use extern in headers for non-inline globals and place the single definition in a .cpp file. Never put a bare non-inline function body in a shared header.

Mark header-defined functions inline. Any function whose body lives in a header included by multiple TUs must be inline (or constexpr, which implies inline for functions since C++11).

Use inline variables for header-defined globals in C++17. Prefer inline constexpr for compile-time constants exposed across TUs. This guarantees a single definition, a stable address, and no ODR violation.

cpp
// C++17 header β€” all three forms are safe across TUs
inline constexpr int kMaxConnections = 128;
inline const std::string kDefaultHost = "localhost"; // C++17
inline std::atomic<int> g_active_count{0};           // C++17

Restrict TU-local symbols with anonymous namespaces. Functions and objects intended only for use within a single TU should have internal linkage. This eliminates the risk of accidentally colliding with a symbol of the same name in another TU.

cpp
namespace {
    void apply_transform(float* data, std::size_t n) { /* TU-local */ }
}

Always guard headers. Include guards or #pragma once prevent a header from being processed more than once per TU, eliminating redeclaration errors for class types and non-inline variable definitions.

Common Pitfalls

Non-inline function definitions in shared headers. Adding a utility function body directly to a .h file causes multiple-definition linker errors the moment a second .cpp file includes it. The fix is inline β€” or moving the definition to a .cpp.

Mismatched extern type and actual definition. Declaring extern float threshold; in a header while defining double threshold = 0.5; in a .cpp constitutes a conflicting declaration. If both declarations are visible in the same TU, the compiler diagnoses it. Across TUs it silently produces undefined behavior, with reads through the wrong type producing garbage values.

Forgotten recompilation. Changing a struct layout in a header and rebuilding only some dependent TUs leaves stale object files with different layout assumptions. The linker cannot diagnose this. The fix is a clean build or a properly configured dependency graph in the build system.

const at namespace scope before C++17. In C++, a const variable at namespace scope has internal linkage by default (since C++98). Defining const int max_retries = 5; in a header gives each TU its own copy β€” safe in terms of ODR, but the variable has multiple addresses, which breaks == comparisons on pointers and wastes space. In C++17, prefer inline constexpr int max_retries = 5; for a single definition with a stable address.

Same-TU Conflicts in Detail

The compiler must reject two declarations in the same scope the moment they are both visible and incompatible. The error messages below are representative of GCC/Clang output.

Variable type mismatch

cpp
// error: conflicting declaration of 'double value'
// note: previous declaration as 'int value'
int value = 0;
double value = 3.14;

The second declaration names the same entity (value in the enclosing scope) but disagrees on the type. The program is ill-formed with a required diagnostic.

Return-type-only overload

Overload resolution in C++ discriminates on parameter types, not return types. Two functions with identical parameter-type-lists but different return types are therefore the same entity declared with different types β€” a conflict, not an overload:

cpp
int   hash(const std::string& s);   // declaration #1
float hash(const std::string& s);   // error: conflicting declaration
                                    //   'float hash(const string&)'
                                    // note: previous declaration was
                                    //   'int hash(const string&)'

GCC reports: error: ambiguating new declaration of 'float hash(...)'. Clang reports: error: functions that differ only in their return type cannot be overloaded. Both reject the program before linking.

Class redefinition within one TU

cpp
struct Config { int level; };

// Later in the same TU or same header included twice without guards:
struct Config { int level; double threshold; }; // error: redefinition of 'struct Config'

Include guards (#ifndef/#pragma once) prevent this from occurring when a header is included more than once.

Cross-TU ODR Violations: Silent Undefined Behavior

The most dangerous ODR violations cross translation-unit boundaries. The standard classifies them as ill-formed, no diagnostic required (IFNDR): a conforming toolchain is permitted β€” but not required β€” to accept the program silently and produce a binary with undefined behavior.

Divergent class layouts

cpp
// network.h β€” included differently in two TUs via a macro guard

#ifdef PROTOCOL_V2
struct Header {
    uint32_t id;
    uint32_t flags;
    uint64_t timestamp;  // extra field in V2
};
#else
struct Header {
    uint32_t id;
    uint32_t flags;
};
#endif

// sender.cpp  β€” compiled with -DPROTOCOL_V2
// receiver.cpp β€” compiled WITHOUT -DPROTOCOL_V2

The linker sees one Header symbol per TU; it has no mechanism to compare field layouts. sender.cpp writes timestamp at offset 8; receiver.cpp never allocates that memory. The resulting behavior β€” silent memory corruption, misread packet data, or an apparent crash elsewhere β€” depends on memory layout and optimization level.

How LTO catches structural mismatches

GCC's link-time optimizer emits -Wodr (enabled by default with -flto) when it detects that two TUs define the same class name with different fields:

g++ -flto -fuse-ld=bfd sender.cpp receiver.cpp

sender.cpp:3:8: warning: type 'struct Header' violates the C++ One Definition Rule [-Wodr]
receiver.cpp:3:8: note: a different type is defined in another translation unit
sender.cpp:5:13: note: the first difference of corresponding definitions is field 'timestamp'
receiver.cpp:3:8: note: a type with different number of fields is defined in another translation unit

Clang's equivalent is -Wlto-type-mismatch. Neither warning fires without LTO.

AddressSanitizer detect_odr_violation

For cross-linkage-unit violations (executable linked against a shared library), AddressSanitizer can catch mismatches at process startup. The runtime registers each global variable and poisons its red zone; if a second registration arrives for the same name, it reports an ODR violation before main runs:

bash
# Build a shared library with one definition, the executable with another
clang++ -fpic -fsanitize=address -shared b.cc -o b.so   # defines: long var;
clang++ -fsanitize=address a.cc ./b.so -o a             # defines: int  var;

Running the executable prints:

==19043==ERROR: AddressSanitizer: odr-violation (0x000001316280):
  [1] size=4 'var' a.cc
  [2] size=8 'var' b.cc

Two flag levels control sensitivity:

ASAN_OPTIONS valueBehavior
detect_odr_violation=1Flag when size differs (int vs long).
detect_odr_violation=2Flag even when size matches (int vs float). Default since Clang 16.

Note that AddressSanitizer instruments global variables, not functions or vague-linkage symbols. A class defined differently in two TUs but with no globally-visible variable will not be caught by this mechanism alone β€” use LTO with -Wodr for structural type mismatches.

extern Declaration Mismatches

A classic ABI mismatch arises when a variable is declared with one type in a header and defined with a different type in a .cpp file. When both declarations are visible to the same TU, the compiler diagnoses the conflict immediately:

cpp
// config.h
extern int max_connections;   // declaration: type is int

// config.cpp
double max_connections = 128; // error: conflicting declaration 'double max_connections'
                              // note:  previous declaration as   'int max_connections'

When the mismatch is split across separately compiled TUs that never see each other's source, the compiler cannot diagnose it:

cpp
// config.h  (included by all TUs)
extern int max_connections;   // everyone thinks it's int

// config.cpp (compiled independently)
double max_connections = 128; // definition β€” a double in memory, 8 bytes
                              // Compiler sees only this TU: no conflict visible

// main.cpp
#include "config.h"
int main() {
    return max_connections; // reads 4 bytes as int from an 8-byte double object
                            // undefined behavior: garbage value or partial read
}

The linker resolves the symbol by name alone, oblivious to the type mismatch. The resulting program may appear to run but will read the wrong bytes. On a little-endian system, the low 4 bytes of an IEEE 754 double holding 128.0 (0x4060000000000000) are 0x00000000, so main silently returns 0 rather than 128. The failure mode changes with the value, the platform, and the optimizer.

Fix: always keep the declaration and definition type-identical, and prefer inline constexpr for compile-time constants to eliminate the extern split entirely.

C++20 Modules and ODR

C++20 named modules introduce a third linkage class β€” module linkage β€” that sits between internal and external linkage and eliminates most cross-TU ODR collisions for module-private entities.

Module linkage

An entity declared inside a named module that is neither exported nor given internal linkage has module linkage: it is visible across all translation units that are part of the same module, but invisible β€” and inaccessible β€” to any TU outside the module:

cpp
// math.cppm β€” primary module interface unit
export module math;

// module-linkage: visible to all TUs in the 'math' module,
// but NOT to any importer of 'math'
double pi_approx = 3.14159265358979;

// external linkage: visible to importers
export double circle_area(double r) { return pi_approx * r * r; }

Because pi_approx has module linkage, a second module defining its own pi_approx introduces no collision with the first one β€” the linker distinguishes them by their module-decorated mangled names.

How modules prevent the classic ODR collision

In the header-era model, two .cpp files that both #include "widget.h" must agree on Widget's definition or produce undefined behavior. With named modules, Widget is defined exactly once inside the module unit that owns it; importers receive a pre-compiled binary module interface (.pcm or .ifc) and never redeclare the type themselves:

cpp
// widget.cppm
export module widgets;

export class Widget { // defined exactly once, inside the module
    int id_;
public:
    explicit Widget(int id) : id_(id) {}
    int id() const { return id_; }
};

// app.cpp
import widgets;       // reuses the compiled module interface β€” no redefinition
// import utils;      // also imports widgets transitively β€” still no redefinition

The module system does not eliminate ODR violations entirely. Two different named modules may each export a class named Bin; if both are imported into the same TU, the program is ill-formed (IFNDR). Compilers that see the conflict directly will diagnose it; those that see only the transitive imports may not. The rule is the same as for headers β€” the standard requires one definition β€” but the structure of modules makes accidental collisions far less likely.

Residual ODR risks with modules

  • Exported inline functions have external linkage, not module linkage. Two modules exporting an inline function of the same name with different bodies produce an ODR violation with the same silent-UB properties as in header code.
  • Global module fragment (module; ... export module foo;): #include directives in this region attach symbols to the global module, not to the named module, and remain subject to the usual header-era ODR rules.
  • Mixed builds: linking module-aware object files with legacy object files from non-module TUs bypasses module-linkage protections for any symbol with external linkage.

Conflicting Declarations Across Translation Units

Extern declarations that mismatch their definition are a silent ODR hazard. The extern keyword promises the compiler that a variable or function exists somewhere β€” it does not enforce that the declared type matches the type of the eventual definition. When the declaration and definition live in different translation units, no single compiler invocation sees both, so the mismatch escapes the front-end and reaches the linker undetected.

cpp
// C++11
// metrics.h β€” seen by every TU
extern std::uint32_t g_request_count;  // declared as 32-bit unsigned

// metrics.cpp β€” compiled alone; compiler never sees metrics.h here
std::uint64_t g_request_count = 0;     // defined as 64-bit unsigned
                                       // linker resolves the name match;
                                       // readers get 4 bytes of an 8-byte object

Linkage class must agree across all declarations of the same name. A function declared static (internal linkage) in one header included into two TUs and also forward-declared without static in a third TU produces a linkage conflict. The Standard makes any program that mixes internal and external linkage declarations for the same entity ill-formed (ODR, Β§6.3):

cpp
// C++11
// helper.h β€” accidentally included in two TUs
static int helper_fn(int x) { return x * 2; }  // internal linkage per TU

// app.cpp β€” forward-declaration without 'static'
int helper_fn(int x);       // external linkage assumed by this declaration
// Both declarations are now for 'helper_fn', but with incompatible linkage.
// Behavior is undefined; GCC may warn: 'helper_fn' declared with internal
// linkage in a header β€” consider anonymous namespace instead.

Prefer anonymous namespaces over static for TU-local symbols in C++11 and later; this guarantees internal linkage without any risk of a stale static-less forward declaration in another header undermining the intent.

Redeclaration vs. Redefinition

Redeclaration is legal; redefinition is not. C++ allows a name to be declared any number of times in the same scope provided each declaration is compatible with the others. A definition creates storage or a body and may appear exactly once per TU for non-inline entities, and exactly once across the entire program for entities with external linkage.

cpp
// C++11
void compress(const std::vector<char>& data);  // declaration #1
void compress(const std::vector<char>& data);  // declaration #2 β€” OK, identical

void compress(const std::vector<char>& data) { // definition β€” OK, first and only
    // ...
}

void compress(const std::vector<char>& data) { // error: redefinition of 'compress'
    // ...
}

Variables can be redeclared with extern but not redefined at namespace scope. Each additional extern declaration is merely a promise; the definition that allocates storage must appear exactly once:

cpp
// C++11
extern int g_timeout;   // declaration
extern int g_timeout;   // redeclaration β€” OK

int g_timeout = 30;     // definition β€” OK, first definition
int g_timeout = 60;     // error: redefinition of 'int g_timeout'

Function parameters, local variables, and class data members are always definitions when declared inside a function body or class body; there is no concept of a redeclaration for them. Attempting to name a local variable twice in the same scope is an immediate error β€” there is no extern-style escape hatch at block scope.

Template Specialization Conflicts

Full specializations must be compatible with the primary template's interface. A full (explicit) specialization of a function or class template must agree with the primary template on all aspects that are not overridden by the specialization itself: return type for function templates, and the template-parameter-list structure for class templates. Introducing a full specialization with an incompatible signature is ill-formed with a required diagnostic:

cpp
// C++11
template<typename T>
T identity(T v) { return v; }

template<>
double identity<double>(double v) { return v; }  // OK: matches primary return type

template<>
int identity<float>(float v) { return static_cast<int>(v); }
// error: return type 'int' of explicit specialization conflicts with
//        return type 'float' of function template 'float identity(float)'

Class template full specializations must be defined before they are used, not after. Defining a full specialization after the point of implicit instantiation for the same arguments is undefined behavior. Place full specializations in a header (or in the same TU as all uses) before any code that could trigger instantiation:

cpp
// C++17
template<typename T>
struct Serializer { void write(const T& v); };

// Using Serializer<int> here would implicitly instantiate the primary.
// A full specialization declared afterwards is too late β€” undefined behavior.

template<>
struct Serializer<int> {      // full specialization β€” must appear before any use
    void write(const int& v); // of Serializer<int>
};

Partial specialization ambiguity is a conflict at instantiation time. When two partial specializations are equally good matches for a given set of template arguments, the compiler cannot pick one and rejects the instantiation:

cpp
// C++11
template<typename T, typename U>
struct Pair {};

template<typename T>
struct Pair<T, T> {};         // specialization A: both types the same

template<typename T>
struct Pair<T*, T*> {};       // specialization B: both types are pointer-to-same

Pair<int*, int*> p;           // error: ambiguous partial specialization;
                              // A matches (T=int*), B also matches (T=int)

Resolving the ambiguity requires adding a third, more-specialized partial specialization that subsumes both, or restructuring the template hierarchy so one of the conflicting partial specializations is removed.

Diagnosing Conflicting Declarations

Compile-time detection covers only conflicts visible within a single translation unit. The compiler can reject a conflicting redeclaration the instant the second declaration appears in the same scope, but it cannot see across TU boundaries. Conflicts that straddle translation units require additional tooling:

Conflict typeDetected atTool
Same-scope type mismatchCompile timeCompiler front-end β€” always a hard error
Return-type-only function overloadCompile timeCompiler front-end β€” always a hard error
Class redefinition in same TUCompile timeCompiler front-end; include guards prevent it
Cross-TU duplicate symbol (non-inline)Link timeLinker β€” reports multiple definition
Cross-TU type/layout mismatchNot detected by defaultLTO -Wodr (GCC) / -Wlto-type-mismatch (Clang)
Cross-TU variable size mismatchRuntime (ASan)ASAN_OPTIONS=detect_odr_violation=2
Stale object file layout divergenceNot detectedClean build; -MMD dependency tracking

Static analyzers extend compile-time coverage for cross-header conflicts. Clang-Tidy's bugprone-mismatched-... family and misc-definitions-in-headers check catch common patterns: non-inline definitions in headers, static data members defined in headers, and certain extern declaration mismatches that are visible across included headers within the same compilation database invocation.

bash
# Run clang-tidy on the whole compilation database to catch header-level conflicts
clang-tidy --checks='misc-definitions-in-headers,bugprone-*' \
           -p build/compile_commands.json src/*.cpp

Sanitizers and LTO together cover the gaps that neither compiler nor linker can catch alone. A robust CI pipeline for a multi-TU codebase should combine all three layers: compilation with warnings enabled (-Wall -Wextra), linking with LTO (-flto) and ODR warnings enabled, and a sanitizer build (-fsanitize=address) with detect_odr_violation=2 for test runs. Each layer catches a different class of conflict that the others miss.

See Also

  • Class Templates β€” ODR rules for template definitions and explicit specializations across translation units
  • ADL (Argument-Dependent Lookup) β€” how declarations in associated namespaces are found during unqualified name lookup