Skip to content
C++
Language
Intermediate

Translation-Unit-Local Entities

Entities with internal linkage or no linkage, confined to a single translation unit and invisible to all others — preventing ODR violations and symbol leakage.

Translation-Unit-Local Entitysince C++20

A translation-unit-local (TU-local) entity is one whose name has internal linkage or no linkage, making it inaccessible by name from any other translation unit; C++20 formalized this concept normatively as part of the module system.

Overview

Every C++ program is composed of one or more translation units — the result of preprocessing a single source file before compilation. Entities declared at namespace scope can have external linkage (visible across all TUs), internal linkage (visible only within their own TU), or no linkage (local to a block). A TU-local entity falls into the internal-linkage or no-linkage category.

Before C++20, "TU-local" was informal shorthand. C++20's module system promoted it to a normative term, with precise rules about which entities qualify and hard restrictions on leaking them through module interfaces.

Why it matters

ODR safety. The One Definition Rule requires exactly one definition of every entity with external linkage across the entire program. Helper functions defined in headers with external linkage violate this the moment they appear in two or more TUs. TU-local entities are exempt from this constraint because they are invisible to other TUs.

ABI surface reduction. TU-local symbols either do not appear in the object file's symbol table or appear only as local symbols. This shrinks binaries, reduces link time, and eliminates unintended public API surface.

Encapsulation beyond classes. Not every implementation detail belongs in a class. File-scoped helpers, internal constants, and private type aliases that are needed in exactly one .cpp file should be TU-local by default, not by convention.

Syntax

static at namespace scope

cpp
// counter.cpp
static int g_count = 0;            // internal linkage — C++98
static void increment() {          // internal linkage — C++98
    ++g_count;
}

static applied to a variable or function at namespace scope gives it internal linkage. This usage is inherited directly from C and has been valid in every C++ standard. It does not apply to types — you cannot give a class or alias internal linkage with static.

Unnamed namespaces

cpp
// lexer.cpp
namespace {
    // All declarations here have internal linkage — C++11 (guaranteed; C++03 had unique external linkage)
    constexpr std::size_t kMaxToken = 512;

    bool is_ident_start(char c) noexcept {         // noexcept — C++11
        return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
    }

    struct ScratchBuffer {
        char data[kMaxToken];
        std::size_t len = 0;
    };
}

In C++11, the standard explicitly mandates internal linkage for names in an unnamed namespace. In C++03 they had external linkage with an unspecified unique name — subtle but important when reasoning about template instantiations. Prefer unnamed namespaces over static for new code: they accommodate types, class templates, and enums, none of which static can make TU-local.

C++20 normative definition

C++20 defines an entity as TU-local if any of the following hold:

  • Its name has internal linkage.
  • It has no name with linkage (local variables, unnamed types, lambdas).
  • It is a template or specialization where a template argument or parameter involves a TU-local entity.
  • It is declared or defined in the global module fragment (before export module) or the private module fragment (module : private;).
cpp
// io.cppm — C++20 module interface unit
module;
#include <cstdio>   // global module fragment: all names from <cstdio> are TU-local   // C++20
export module io;

namespace {
    int to_posix_flags(int mode) { /* ... */ return mode; } // TU-local   // C++20
}

export int open_file(const char* path, int mode) {
    return to_posix_flags(mode); // fine: call TU-local from an exported function   // C++20
}

Examples

File-scoped implementation helpers

cpp
// http_response.cpp
#include "http_response.hpp"
#include <charconv>       // C++17
#include <string_view>    // C++17

namespace {
    bool has_prefix(std::string_view s, std::string_view prefix) noexcept {
        return s.size() >= prefix.size() &&
               s.substr(0, prefix.size()) == prefix;
    }

    int parse_status_code(std::string_view tok) noexcept {
        int code = 0;
        auto [ptr, ec] = std::from_chars(             // C++17
            tok.data(), tok.data() + tok.size(), code);
        return ec == std::errc{} ? code : -1;
    }
}

HttpResponse parse(std::string_view raw) {
    if (!has_prefix(raw, "HTTP/")) return {};
    // parse_status_code is invisible to every other TU
    auto status = parse_status_code(raw.substr(9, 3));
    return HttpResponse{status};
}

has_prefix and parse_status_code can be defined identically in websocket_parser.cpp without any link conflict. Each TU gets its own, independent copy.

Guarding against ODR violations in header-only code

cpp
// PROBLEM: helper with external linkage included in multiple TUs
// utils.h — C++98
bool validate(int x) { return x >= 0; } // ODR violation when included twice

// CORRECT: inline resolves ODR for functions — C++98
inline bool validate(int x) { return x >= 0; }

// ALSO CORRECT: unnamed namespace, but only usable in one TU
// (do not put this in a widely-included header)
namespace {
    bool validate(int x) { return x >= 0; } // TU-local copy per TU
}

For headers included in many TUs, inline is the right fix. Unnamed namespaces in headers create a separate copy per TU, which is sometimes intended (e.g., a header providing a default policy) but usually a mistake that silently inflates binary size.

C++20: preventing header names from polluting a module's interface

cpp
// renderer.cppm   // C++20
module;
// All #includes belong in the global module fragment.
// Their declarations are TU-local and will not leak to importers.
#include <vulkan/vulkan.h>
#include <glm/glm.hpp>
export module renderer;

// VkInstance, glm::mat4, etc. are TU-local here.
// Exported functions may USE them internally but cannot expose them in signatures.

export class Renderer {
public:
    void draw_frame();
    // Cannot have: VkInstance get_instance(); — VkInstance is TU-local
private:
    struct Impl;           // pimpl keeps Vulkan types out of the interface
    Impl* impl_ = nullptr;
};

This is the canonical pattern for wrapping C libraries in C++20 modules. The #include lives in the global module fragment; the module interface exposes only C++ types it controls.

Best Practices

  • Unnamed namespace by default for every non-inline helper in a .cpp file. Use static only when porting C code or when the intent needs to be unmistakably explicit.
  • Never define non-inline, non-template functions in headers with external linkage unless you mean for all TUs to share a single definition (which requires a matching declaration in exactly one .cpp).
  • In C++20 module interfaces, every #include belongs in the global module fragment. Omitting module; and including directly inside the module unit makes those declarations part of the module's interface, exposing types you do not own.
  • Audit link maps for surprises. Symbols that should be TU-local but appear with external linkage in a link map are encapsulation leaks. nm --defined-only -g on an object file reveals them.

Common Pitfalls

Template definitions in unnamed namespaces across TUs

cpp
// a.cpp
namespace {
    template<typename T>
    T clamp(T v, T lo, T hi) { return v < lo ? lo : (v > hi ? hi : v); }
}
// b.cpp — identical definition, different TU-local template
namespace {
    template<typename T>
    T clamp(T v, T lo, T hi) { return v < lo ? lo : (v > hi ? hi : v); }
}

Each TU gets its own independent template. Instantiating clamp<int> in both TUs produces two separate symbol definitions, not an ODR violation (they are distinct entities), but the duplication inflates binary size. Shared templates belong in a header with external linkage; only non-template helpers go in unnamed namespaces.

Exposing TU-local types from a module interface (C++20)

cpp
module;
struct InternalHandle { int fd; };  // TU-local   // C++20
export module io;

export InternalHandle open(const char*); // ill-formed: return type is TU-local

Importers of io cannot name InternalHandle, so the exported function's signature is unusable. Compilers are required to diagnose this. The fix is a pimpl, an opaque pointer, or moving InternalHandle into the module interface (outside the global module fragment).

Confusing static member functions with static linkage

cpp
struct Codec {
    static void encode(const char*, std::size_t); // static member — external linkage, no `this`
};

static void encode(const char*, std::size_t);     // free function — internal linkage

These look similar but behave entirely differently. static on a member gives the function class scope without an implicit this; it does not affect linkage. Only static at namespace scope produces internal linkage.

static at Namespace Scope: History and Scope

What static does — and does not do — at namespace scope

Applying static to a variable or function at namespace scope is the oldest mechanism for internal linkage in C++, inherited verbatim from C89. It tells the compiler that the name belongs solely to this translation unit; the linker will never merge it with an identically named symbol in another object file.

cpp
// config.cpp
static const char* kDefaultHost = "localhost";   // internal linkage — C++98
static int connect(const char* host) { /* ... */ return 0; }  // internal linkage

Two critical boundaries bound this mechanism:

  1. static cannot give internal linkage to a type, class template, or enum. static struct Foo {}; is ill-formed.
  2. static on a class member is entirely unrelated — it gives the member class scope with no implicit this, but does not affect linkage at all. A static member function has external linkage by default.
cpp
struct Encoder {
    static void encode(const char* p, std::size_t n);  // external linkage, no this
};

static void encode(const char* p, std::size_t n);      // free function, internal linkage

The declarations look nearly identical in isolation. The distinction is syntactic context: inside a class body vs. at namespace scope.

Historical C usage vs. modern anonymous namespace preference

In C, static at file scope is the only way to restrict a symbol's visibility. C++ inherited this and it remains fully valid, but the language committee introduced anonymous namespaces in the 1998 standard precisely because static is insufficient for types. Modern C++ style guides — including the C++ Core Guidelines — recommend reserving namespace-scope static for code that must also compile as C or when a single-variable declaration needs to be unmistakably self-documenting.

The anonymous namespace is the idiomatic replacement for everything else:

cpp
// Prefer this in new C++ code
namespace {
    const char* kDefaultHost = "localhost";
    int connect(const char* host) { /* ... */ return 0; }
    struct ConnectionState { int fd; bool alive; };   // types work here
    template<typename T> T clamp_val(T v, T lo, T hi);  // so do templates
}

Anonymous Namespaces: The Modern C++ Idiom

An unnamed (anonymous) namespace is syntactic sugar for a namespace with a compiler-generated, TU-unique name, followed by an implicit using directive that brings all its members into the enclosing scope. The net effect: every declaration inside has internal linkage.

cpp
namespace {
    // Equivalent to:
    // namespace __unique_name_57a3f { ... }
    // using namespace __unique_name_57a3f;
}

Since C++11, the standard explicitly mandates internal linkage for these names (C++03 gave them external linkage with an unspecified unique name, which caused subtle problems with template instantiations). When targeting C++11 or later, anonymous namespaces are safe and correct for all TU-private entities.

What you can make TU-local that static cannot

cpp
namespace {
    // All of the following are impossible with namespace-scope `static`:

    struct ParseState {             // type with internal linkage
        std::size_t pos = 0;
        bool error = false;
    };

    enum class TokenKind {          // scoped enum with internal linkage
        Ident, Number, Punct, Eof
    };

    template<typename Char>        // function template with internal linkage
    bool is_whitespace(Char c) noexcept {
        return c == ' ' || c == '\t' || c == '\n' || c == '\r';
    }

    using CharBuf = std::array<char, 256>;  // type alias with internal linkage
}

Redundant static inside an anonymous namespace

Placing static on a declaration already inside an anonymous namespace is legal but redundant — the name already has internal linkage from the namespace. Compilers do not warn about this, but it is noise:

cpp
namespace {
    static int counter = 0;       // static is redundant; counter is already TU-local
    int counter2 = 0;             // identical effect
}

Prefer the form without static; the namespace declaration makes intent clear enough.

Anonymous namespaces in headers

Putting an anonymous namespace in a header that is included by many translation units creates one independent copy of each declared entity per TU. This is almost never what you want for shared logic:

  • Binary size grows: n TUs each compile their own copy of the function.
  • Each copy is a distinct entity: mutations to a TU-local variable in one TU are invisible to others.
  • If the header is included in the same TU twice (uncommon but possible without include guards), the name is still unique per TU — no conflict.

The correct pattern is inline for functions and inline constexpr for constants (C++17).


C++20 Modules and TU-Local Entities

C++20 modules introduce a third linkage category that sits between internal and external linkage: module linkage. Understanding the difference between module linkage and internal linkage is essential for writing correct module interfaces.

Module linkage: visible within the module, not exported

A name declared in a module unit without export has module linkage. It is visible to all other units of the same named module (the interface unit and all its implementation units), but inaccessible to importers.

cpp
// math.cppm — module interface unit   // C++20
export module math;

// No export: module linkage — any math implementation unit can use this
double pi_approx(int terms) { /* Leibniz series */ return 0.0; }

// Exported: external linkage — importers can call this
export double circle_area(double r);
cpp
// math_impl.cpp — implementation unit   // C++20
module math;

// pi_approx is visible here even though it is not exported
double circle_area(double r) {
    return pi_approx(1000) * r * r;  // legal
}

Internal linkage within a module unit

A name inside an anonymous namespace within a module unit has internal linkage — it is invisible even to other units of the same module:

cpp
// math.cppm   // C++20
export module math;

namespace {
    // Internal linkage: ONLY visible in this particular TU.
    // NOT visible in math_impl.cpp.
    double kEpsilon = 1e-9;
}

double pi_approx(int terms);   // module linkage: visible to math_impl.cpp

The difference at a glance

LinkageDeclared howVisible to importers?Visible to same-module units?
Externalexport in a module interfaceYesYes
ModuleIn a module unit, no exportNoYes
InternalIn anonymous namespace, or staticNoNo

This three-way distinction is unique to C++20 modules. In the traditional header model only internal and external linkage exist.

TU-local entities must not leak through module interfaces

C++20 adds a hard constraint: an exported function or variable may not expose a TU-local type in its signature. If an entity from the global module fragment (before module; is closed) or from an anonymous namespace appears as a return type, parameter type, or base class in an exported declaration, the program is ill-formed and the compiler must diagnose it.

cpp
module;
#include <sys/types.h>   // off_t is now TU-local   // C++20
export module fileio;

export off_t file_size(const char* path);  // ERROR: off_t is TU-local

Fix: wrap the type, use a type alias defined after export module, or restrict the C header to implementation units.


[[deprecated]] on TU-Local Entities

The [[deprecated]] attribute (C++14) is fully valid on entities with internal linkage. It instructs the compiler to emit a diagnostic when the deprecated entity is used — and since a TU-local entity can only be used within the same TU, any diagnostic will appear in exactly that TU.

cpp
// legacy_parser.cpp
namespace {
    [[deprecated("Use parse_v2 instead")]]   // C++14
    std::string parse_v1(std::string_view input) {
        // old implementation
        return std::string(input);
    }

    std::string parse_v2(std::string_view input);
}

void process(std::string_view data) {
    auto result = parse_v1(data);   // compiler warns here, within this TU
}

This pattern is useful when incrementally refactoring a large .cpp file: annotate the old internal helper as deprecated, add the replacement, let the compiler enumerate every remaining call site without affecting other translation units.


ODR and TU-Local Entities

Why internal linkage is exempt from the ODR

The One Definition Rule states that every entity with external linkage that is used must have exactly one definition across the entire program. TU-local entities with internal linkage are entirely outside this constraint. The linker treats each TU's internal symbol as an independent, unrelated entity, so identical definitions in two different TUs are not ODR violations — they are two distinct entities that happen to share the same source text.

cpp
// tokenizer.cpp
namespace {
    bool is_digit(char c) noexcept { return c >= '0' && c <= '9'; }
}

// parser.cpp
namespace {
    bool is_digit(char c) noexcept { return c >= '0' && c <= '9'; }
}
// Two definitions, two distinct entities, zero ODR concern.

Both object files will contain a local is_digit symbol. The linker never sees a conflict because neither symbol is exported to the global symbol table.

Contrast with external linkage

An externally linked function defined in two .cpp files triggers either a linker error (duplicate symbol) or — for inline or template entities where the definitions are supposed to be identical — silent undefined behavior if the definitions diverge:

cpp
// WRONG: external linkage, two definitions → ODR violation
// file_a.cpp
int helper() { return 1; }

// file_b.cpp
int helper() { return 2; }
// Linker may pick either; behavior of callers in other TUs is undefined.

The TU-local version never has this problem. Each TU's copy is fully self-contained.

Binary size implication

Exemption from the ODR is not free. Every TU that defines the same internal helper emits its own copy into the object file. The linker cannot merge them. For small helpers this is usually acceptable; for larger algorithms shared across many TUs, promote them to an externally linked function in its own .cpp file.


constexpr Variables: Linkage Through the Standards

C++11–C++14: constexpr implies internal linkage at namespace scope

In C++11 and C++14, a constexpr variable at namespace scope has internal linkage by default — the same rule that applies to const. Each TU that includes the header defining it gets its own independent copy:

cpp
// constants.hpp (C++11/14)
constexpr double kGravity = 9.80665;   // internal linkage: one copy per TU

This is fine for pure compile-time constants used in templates or array sizes, where the value is consumed at compile time and no runtime address is needed. If two TUs take the address of kGravity, they get two distinct pointers, which matters when comparing addresses across TU boundaries.

C++17: inline constexpr — external linkage, one address

C++17 introduced inline variables (not just inline functions), allowing a constexpr variable in a header to have external linkage. All TUs that include the header share a single definition with a single address:

cpp
// constants.hpp (C++17)
inline constexpr double kGravity = 9.80665;   // C++17: external linkage, one object

Use inline constexpr whenever the constant might be passed by reference, stored in a pointer, or compared by address across TU boundaries. For compile-time-only values used as template arguments, plain constexpr is still sufficient.

C++20: constinit — static initialization without forced const

constinit (C++20) asserts that a variable with static or thread-local storage duration is initialized with a constant expression, but does not imply const or affect linkage. Linkage is determined separately:

cpp
// C++20
constinit int g_connection_limit = 256;          // namespace scope: external linkage
static constinit int g_local_limit = 64;         // internal linkage via static
constinit inline int g_shared_limit = 128;       // external linkage via inline (C++17+)

A common confusion: constinit does not make a variable const, and it does not change linkage. It is strictly an initialization-time assertion.

Summary table: const/constexpr/constinit linkage

SpecifierStandardNamespace-scope linkageNotes
constC++98InternalUnlike C; safe in headers
constexprC++11InternalImplies const; safe in headers
inline constexprC++17ExternalOne definition, one address
constinitC++20External (default)Linkage from context, not specifier
static constinitC++20Internalstatic overrides default
inline constinitC++20ExternalExplicit external + const-init

See Also

  • reference/language/unnamed-namespace — the canonical C++ mechanism for TU-local entities
  • reference/language/odr-linkage — why internal linkage exempts entities from the One Definition Rule
  • reference/language/modules — C++20 modules and the normative TU-local definition
  • reference/language/conflicting-declarations — link-time conflicts that TU-locality prevents