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.

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