5.2.31 · D2C++ Programming

Visual walkthrough — Inline namespaces, anonymous namespaces

1,917 words9 min readBack to topic

This page is a companion to Inline namespaces, anonymous namespaces. It leans on Namespaces (basics), Translation units and the One Definition Rule, and Internal vs external linkage.


Step 1 — What is a "name" once compiling starts?

WHAT: We split the world into two rooms — the compiler (works on one file) and the linker (glues all the finished files together).

WHY: Every rule about namespaces is really a rule about which room can see which box. If we don't picture the two rooms, the rules look like arbitrary magic.

PICTURE: Look at Step 1. Two files each become a TU; each TU produces boxes; the linker at the bottom collects every box into one table.

Figure — Inline namespaces, anonymous namespaces

Step 2 — Linkage: which boxes are visible to the linker?

WHAT: We tag every box as "external" (public label) or "internal" (private label).

WHY: A One Definition Rule violation — the dreaded "multiple definition" link error — happens only when two external boxes share the same label. Internal boxes can never collide, because the linker never even sees them.

PICTURE: In Step 2, external boxes have a solid orange handle reaching into the shared table; internal boxes have a teal padlock and no handle.

  • x — a plain global: its label goes public, so two files defining x clash.
  • y — marked static at file scope: label stays private, so two files each with static int y are fine.
Figure — Inline namespaces, anonymous namespaces

Step 3 — The anonymous namespace = an auto-generated private room

WHAT: We write a namespace with no name and drop our helper inside it.

namespace {          // no name
    int secret = 42;
}

WHY: We want the internal-linkage effect of static, but for anything — variables, functions, types, templates. static cannot make a struct file-local; the unnamed namespace can.

HOW (the mental rewrite): The compiler treats the code above as if you wrote a namespace with a secret, per-file, one-of-a-kind name, then immediately pulled it into scope:

  • __TU7a — an invented label different in every .cpp. Because file A's is __TU7a and file B's is __TU9k, their secrets have different mangled symbols (see Name mangling and the ABI) and can never clash.
  • using namespace __TU7a; — implicitly added so you can still just type secret without qualification inside this file.

PICTURE: Step 3 shows two files; each grows its own uniquely-labeled private room around secret. The two rooms never touch.

Figure — Inline namespaces, anonymous namespaces

Step 4 — Edge case: the same helper name in two files

WHAT: Both logger.cpp and metrics.cpp define const char* tag.

WHY: This is exactly the collision Step 2 warned about — unless we wrap each in an anonymous namespace.

PICTURE: Step 4 is a side-by-side. Left: bare globals → two identical labels hit the linker table → red CLASH. Right: each wrapped in its unnamed namespace → two distinct mangled labels → green OK.

The two mangled strings differ in the invented middle piece, so the linker files them as two unrelated boxes.

Figure — Inline namespaces, anonymous namespaces

Step 5 — Now the opposite goal: we WANT a name to escape

WHAT: Instead of hiding, we want lib::v2::process to also answer to the shorter name lib::process.

WHY: API versioning. Customers wrote lib::process(). We want their code to float forward to the newest version with zero edits, while a customer who pinned lib::v1::process() keeps the old behavior.

namespace lib {
    inline namespace v2 { void process(); }   // current default
    namespace v1        { void process(); }   // opt-in legacy
}

PICTURE: Step 5 shows the lib room containing two sub-rooms, v1 (closed door) and v2 (open archway labeled inline). Through the archway, process is also visible on the lib wall.

Figure — Inline namespaces, anonymous namespaces

Step 6 — What inline actually does to lookup

WHAT: We trace three ways a caller can write the name and where each lands.

Caller writes Resolves to WHY
lib::process() lib::v2::process() v2 is inline → its members leak into lib
lib::v2::process() lib::v2::process() explicit, same box
lib::v1::process() lib::v1::process() explicit opt-in; v1 is not inline

WHY it beats a plain using: A using namespace v2; would make the name visible, but lib::v2::T and lib::T would stay different entities for template specialization and for the mangled symbol. inline fuses them, and the version gets baked into the linker symbol (_ZN3lib2v17processEv) — see Name mangling and the ABI.

PICTURE: Step 6 is a lookup flow: the query lib::process walks into lib, sees the inline archway, and arrives at v2::process; the same box carries a mangled label with 2v2 embedded.

Figure — Inline namespaces, anonymous namespaces

Step 7 — Edge case: ADL sees through the archway

WHAT: An unqualified call draw(w) where w's type lives in an inline namespace.

namespace ns {
    inline namespace impl { struct Widget {}; void draw(Widget){} }
}
ns::Widget w;
draw(w);   // found — no ns:: needed

WHY: Argument-Dependent Lookup (ADL) finds functions in the associated namespaces of an argument's type. Because impl is inline, Widget's associated namespace effectively includes ns, so draw is reachable as if it lived directly in ns.

PICTURE: Step 7: the argument w sends dotted "search here" rays into both impl and — through the inline archway — into ns, catching draw.

Figure — Inline namespaces, anonymous namespaces

Step 8 — Edge case: two libraries, both inline namespace v1

WHAT: LibFoo and LibBar each ship inline namespace v1. Do they merge?

WHY: People fear silent mixing. But the inline name is part of the mangled symbol, and the two libraries live in different enclosing namespaces (foo::v1::... vs bar::v1::...), so their symbols differ. Even same-library, different-version objects produce different mangled symbols → the linker keeps them apart or fails loudly instead of silently calling the wrong code. That is the intended ABI-tagging (see static keyword (storage & linkage) for the older, coarser tool this replaces).

PICTURE: Step 8: two symbol strings side by side, the differing version segment highlighted in plum — proof they never collapse into one box.

Figure — Inline namespaces, anonymous namespaces

The one-picture summary

Everything compresses to a single question at each box: does its label leave the file, and if so, under what name?

Figure — Inline namespaces, anonymous namespaces
  • Anonymous namespace → padlock: label wrapped in a per-file secret name, never reaches the shared table → no collisions possible.
  • Plain namespace → labeled box in the shared table under its full path.
  • Inline namespace → labeled box plus a shortcut arrow: the enclosing name works too, and the version is stamped into the symbol.
Recall Feynman retelling of the whole walkthrough

Every file is a room. When a room finishes, it hands the linker a stack of boxes with names on them. Two rooms handing over boxes with the same name causes a fight (a link error).

An anonymous namespace is a private drawer inside one room: the compiler secretly renames everything in it with that room's unique tag, so even if two rooms both keep a box called "tag," their real names differ and no fight ever happens. (Never put that drawer in a header, because the header is copied into every room, giving each its own separate drawer.)

An inline namespace is the opposite wish: you have a box in a labeled cupboard v2 inside the big cupboard lib, and you leave the v2 door propped open so the box also answers to the plain lib name. New visitors who just ask for lib::process walk straight to the newest version; old visitors who insist on lib::v1::process still find the old one behind its closed door. And because the version is written into the box's real name, boxes from different versions never get confused — that is the safety feature, not a bug.


Recall

Empty label test
A name in an unnamed namespace gets internal linkage — its symbol is never listed in the shared linker table.
One-keyword release
To make a new version the default, move the inline keyword to it.