Skip to content
jieqi's archive
Go back

C++: ODR and Linkage

Contents

This post assumes you know how C++ compiles.

Two rules decide what happens when the same name appears in more than one object file: the One Definition Rule, and linkage. Most people run on intuition, “don’t define the same thing twice” and “static makes it file-private”, which is why inline on a header function reads as an incantation.

This post covers declarations versus definitions, what ODR requires, and the three kinds of linkage. The exceptions, inline, templates and weak symbols, come next.

Declarations vs. Definitions

A declaration introduces a name and its type (“this exists, here’s its shape”) without necessarily creating anything.

A definition brings the entity into existence: for a variable, space is reserved; for a function, the body exists; for a class, the full layout is known.

Every definition is also a declaration. Not every declaration is a definition.

extern int x;              // declaration only
int x = 5;                 // definition

int add(int a, int b);     // declaration only
int add(int a, int b) { return a + b; }  // definition

class Foo;                 // declaration only
class Foo { int y; };      // definition

This distinction is exactly what ODR and linkage are stated in terms of: a TU can declare something any number of times, but define it at most once.

Under the hood, a declaration emits nothing: it’s just bookkeeping the compiler uses for type-checking, gone by the time the object file is written. A definition emits real bytes at a real address. Take:

// a.cpp
extern int x;   // declaration
// b.cpp
int x = 5;      // definition
a.o
  .data:          (nothing)
  symbol table:   x → UNDEFINED

b.o
  .data:          05 00 00 00
  symbol table:   x → defined, address .data+0x00

a.cpp reserves nothing, since extern says “this lives elsewhere.” b.cpp carves out 4 bytes and records their address. Linking just matches the two up: undefined reference in a.o, defined address in b.o, patch one to point at the other.

This is also where the two classic linker errors come from: no definition anywhere → undefined reference. Two definitions of the same external-linkage name → multiple definition. Both are the same underlying fact from opposite sides: there must be exactly one real address per entity, program-wide.

The One Definition Rule

ODR comes in two parts.

Rule 1: one definition per translation unit

Expand

Within a single TU, any entity can be defined at most once, whatever its linkage: variables, functions, classes, templates, all of it.

// a.cpp
int x = 5;
int x = 6;   // error: redefinition of 'x'

The compiler catches this itself: both definitions are in the same file.

Rule 2: one definition per program

Expand

Across the whole program, any entity with external linkage can be defined at most once.

// a.cpp
int x = 5;

// b.cpp
int x = 6;   // compiles fine on its own...

Both files compile, since the compiler only ever sees one TU at a time. The linker sees both, finds two definitions of x, and reports multiple definition of 'x'.

Internal linkage never trips Rule 2. static int x in each file is two separate entities that share a name, not one entity defined twice.

Declarations, meanwhile, are unrestricted: extern int x; can appear in as many TUs as you like, as many times as you like. Only definitions are limited.

An exception

Rule 2 has a carve-out: some entities, like classes, templates and inline functions, may be defined in several TUs, as long as every definition is identical. That’s what lets you put a class definition in a header and include it everywhere. How that works, and what happens when the copies don’t match, is the next post.

Linkage

Linkage answers one question: if the same name appears in more than one place, do those appearances refer to the same entity? There are three kinds.

No linkage

The name can only be referred to from the scope it’s declared in, so the linker never has anything to match up. That covers local variables and anything else declared inside a function body.

void f() {
    int x = 5;   // no linkage: only exists inside f
}

Internal linkage

Visible throughout its own translation unit, invisible to every other one. Two TUs can each have an internal-linkage x, and they’re two unrelated entities that happen to share a spelling. The linker never tries to unify them.

// a.cpp
static int counter = 0;         // private to a.cpp
namespace { int helper = 1; }   // also private to a.cpp
const int MAX = 100;            // internal by default

External linkage

The name refers to the same entity across the whole program. Every TU that declares it is talking about one shared thing, and the linker matches all of those declarations to exactly one definition.

// shared.h
extern int total;      // declaration: "total exists somewhere"
int add(int, int);     // declaration

// shared.cpp
int total = 0;         // the one definition
int add(int a, int b) { return a + b; }

This is where ODR’s program-wide rule bites. Internal linkage can’t violate it: each TU’s copy is its own entity, with no shared identity to protect. But an external-linkage entity defined in a header that five .cpp files include is five definitions of one entity, and the linker rejects it.

Next Post
C++: Initialisation Methods