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++20A 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
// 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
// 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;).
// 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
// 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
// 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
// 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-
inlinehelper in a.cppfile. Usestaticonly 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
#includebelongs in the global module fragment. Omittingmodule;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 -gon an object file reveals them.
Common Pitfalls
Template definitions in unnamed namespaces across TUs
// 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)
module;
struct InternalHandle { int fd; }; // TU-local // C++20
export module io;
export InternalHandle open(const char*); // ill-formed: return type is TU-localImporters 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
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 linkageThese 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.
See Also
reference/language/unnamed-namespace— the canonical C++ mechanism for TU-local entitiesreference/language/odr— why internal linkage exempts entities from the One Definition Rulereference/language/modules— C++20 modules and the normative TU-local definitionreference/language/conflicting-declarations— link-time conflicts that TU-locality prevents