5.2.31 · D4C++ Programming

Exercises — Inline namespaces, anonymous namespaces

2,330 words11 min readBack to topic

Two words we will lean on repeatedly, defined once so no symbol is unearned:


Level 1 — Recognition

Exercise 1.1

What linkage do the names inside an anonymous namespace have?

Recall Solution

Internal linkage. Every name declared inside an unnamed namespace is visible only within the TU that declared it. The compiler behaves as if it invented a unique hidden name per TU plus an implicit using directive, so two different .cpp files can never see each other's copies.

Exercise 1.2

In the code below, which single word makes process reachable as lib::process()?

namespace lib {
    inline namespace v2 { void process(); }
}
Recall Solution

The keyword ==inline==. Because v2 is an inline namespace, its members leak up into the enclosing namespace lib, so lib::process() resolves to lib::v2::process().

Exercise 1.3

True or false: an anonymous namespace is a good place to declare something inside a header file.

Recall Solution

False. A header is #included into many TUs; each inclusion produces a separate internal-linkage copy. See Exercise 2.2 for the concrete damage this causes.


Level 2 — Application

Exercise 2.1

Two files each define a file-local helper. Will they link?

// a.cpp
namespace { int compute() { return 1; } }
// b.cpp
namespace { int compute() { return 2; } }
Recall Solution

Yes, they link cleanly. Each compute sits in a different invented unnamed namespace (one per TU), so the linker sees two distinct symbols — no clash, no violation of the One Definition Rule. Had both been written as global int compute() with external linkage, the linker would report "multiple definition of compute."

Exercise 2.2

You put namespace { int counter = 0; } in util.h and include it in three .cpp files, expecting one shared counter. How many independent counter objects actually exist, and why?

Recall Solution

Three independent objects — one per including TU. The unnamed namespace forces internal linkage, and internal linkage means each TU gets its own private copy. Incrementing the counter in one file does nothing to the others, and the binary carries three separate variables. Fix: keep the shared counter with external linkage — declare extern int counter; in the header and define it in exactly one .cpp.

Exercise 2.3

Given the versioned library below, what does each call resolve to?

namespace mathx {
    namespace v1 { double area(double r){ return 3.14 * r * r; } }
    inline namespace v2 { double area(double r){ return 3.14159 * r * r; } }
}
double a = mathx::area(2.0);
double b = mathx::v1::area(2.0);
Recall Solution
  • mathx::area(2.0) → the inline v2 version, since v2 leaks into mathx. Value: .
  • mathx::v1::area(2.0) → the explicit legacy v1. Value: . Default callers float forward to v2 for free; anyone who pinned v1 is untouched.
Figure — Inline namespaces, anonymous namespaces

Level 3 — Analysis

Exercise 3.1

Explain, in terms of what the compiler conceptually inserts, why an unnamed namespace can never collide across files, whereas a global name might.

Recall Solution

For each TU the compiler acts as if it wrote:

namespace __unique_per_TU {  /* your names */  }
using namespace __unique_per_TU;   // implicit

The invented name __unique_per_TU differs in every .cpp. So secret in file A is really __uniqueA::secret and in file B is __uniqueB::secret — different fully-qualified names ⇒ different linker symbols ⇒ no collision. A global name has one shared fully-qualified spelling and external linkage, so two definitions map to the same symbol and the linker rejects the duplicate.

Exercise 3.2

Does unqualified draw(w) compile below? Justify using Argument-Dependent Lookup (ADL).

namespace ns {
    inline namespace impl { struct Widget {}; void draw(Widget){} }
}
ns::Widget w;
draw(w);   // no ns:: prefix
Recall Solution

Yes. ADL says: to resolve an unqualified function call, also search the namespaces associated with the argument types. Widget's associated namespace is ns::impl. Because impl is inline, its contents are treated as members of ns too, so the associated set effectively includes ns, and draw (which lives in impl/ns) is found. Had impl been a plain (non-inline) namespace, draw would still be found via ADL because it's in the same associated namespace as Widget — the inline keyword's real payoff shows up when you call ns::draw(w) qualified, which only works because inline leaks draw up into ns.

Exercise 3.3

Why does moving the inline keyword from v2 to v3 correctly re-route both source-level calls and linker symbols, without touching caller code?

Recall Solution

Two layers change together:

  1. Source resolution: the unqualified name mathx::area now leaks up from whichever namespace is inline. Move inline to v3 and every mathx::area call recompiles to v3::area — a one-keyword diff, no caller edits.
  2. Linker symbols (ABI): the inline namespace name is baked into the mangled symbol, e.g. _ZN5mathx2v34areaEd vs _ZN5mathx2v24areaEd. So an old object file that was compiled against v2 keeps referencing the v2 symbol; it does not silently start calling v3. Version is encoded end-to-end.

Level 4 — Synthesis

Exercise 4.1

Design a library net that ships an experimental v3 while v2 stays the default, and a stable v1 remains callable. Write the namespace skeleton and state what each of these resolves to: net::send(), net::v3::send(), net::v1::send().

Recall Solution
namespace net {
    namespace v1 { void send(); }          // legacy, opt-in
    inline namespace v2 { void send(); }    // current DEFAULT
    namespace v3 { void send(); }          // experimental, opt-in
}
  • net::send()net::v2::send() (only v2 is inline, so only it leaks up).
  • net::v3::send() → the experimental version, reachable only by explicit qualification.
  • net::v1::send() → the legacy version, explicit. When v3 is ready to become default you move inline from v2 to v3; existing net::send() calls migrate on recompile, pinned callers of v1/v2 are unaffected.

Exercise 4.2

You have a header-based, single-file library where every symbol must be file-local to avoid clashing with the host program, including a helper struct Node. Explain why you cannot use static for Node, and write the correct construct.

Recall Solution

static grants internal linkage only to variables and functions; a type like struct Node has no linkage to make static in this sense — the keyword simply isn't allowed to give a class internal linkage. The uniform tool is the unnamed namespace, which gives internal linkage to everything, types included:

// helper.cpp   (NOT a header — anonymous namespaces stay in .cpp)
namespace {
    struct Node { int value; };
    Node makeNode(int v) { return Node{v}; }
}

Level 5 — Mastery

Exercise 5.1

A team writes namespace { int cache; } in a widely-included header and reports two symptoms: (a) the executable is unexpectedly large, and (b) writes to cache in one file are invisible in another. Diagnose both symptoms with a single root cause, then give the corrected design.

Recall Solution

Root cause: internal linkage × N inclusions. The unnamed namespace makes cache internal-linkage, and the header is included by, say, N = 5 TUs. Result: 5 independent cache objects.

  • Symptom (a), bloat: 5 separate variables (and any functions in that namespace) are emitted, one per TU.
  • Symptom (b), no sharing: each TU reads/writes its own copy, so cross-file communication silently fails. Corrected design — one shared object with external linkage:
// cache.h
extern int cache;          // declaration only
// cache.cpp
int cache = 0;             // single definition, external linkage

Now exactly one cache exists and all TUs share it. Rule of thumb: anonymous namespaces live in .cpp files; shared state lives behind an extern declaration + one definition.

Exercise 5.2

Predict the printed output. Then explain which mechanism (inline leak, ADL, or per-TU uniqueness) each line exercises.

namespace app {
    namespace v1 { int scale(int x){ return x * 2; } }
    inline namespace v2 { int scale(int x){ return x * 10; } }
    inline namespace v2 { struct Vec { int n; }; int norm(Vec v){ return v.n; } }
}
int main() {
    app::Vec v{7};
    std::cout << app::scale(3)     << "\n";   // line A
    std::cout << app::v1::scale(3) << "\n";   // line B
    std::cout << norm(v)           << "\n";   // line C  (no app::)
}
Recall Solution

Output:

30
6
7
  • Line A → 30. app::scale resolves through the inline leak to v2::scale, so .
  • Line B → 6. Explicit app::v1::scale opts into legacy: .
  • Line C → 7. norm(v) is unqualified. Vec's associated namespace is app::v2, which is inline (so also app); ADL finds norm. It returns v.n = 7.

Recall Master checklist

Anonymous namespace gives which linkage? ::: Internal — per-TU private. Where must anonymous namespaces never appear? ::: In headers (each include = a separate copy). lib::process() with inline namespace v2 resolves to? ::: lib::v2::process(). To promote a new default version you move which keyword? ::: inline. Does the inline namespace name enter the mangled symbol? ::: Yes — it is ABI-tagged. Can static give a struct internal linkage? ::: No; use an unnamed namespace instead.