Namespaces

C++ namespaces organize names into scopes; understand qualified lookup, using declarations, ADL, and linkage boundaries to keep library APIs predictable.

level intermediate time 11 min at Standard depth
version C++23
what

A namespace puts type, function, and object names in a named scope so unrelated code needn’t compete for one global name.

trap

using namespace exposes unqualified lookup to a whole namespace. Put it in a header, and every includer inherits that effect.

fix

Use qualified names in public interfaces and prefer a single-name using declaration in local code. Check argument-dependent lookup (ADL) explicitly around customization points.

What it is and why it exists

A C++ program uses the standard library, third-party libraries, and project code together. Each may reasonably need names such as parse, Status, or Config. Namespaces place those declarations in different scopes so wire::Status and storage::Status can coexist.

A namespace is a declarative region, not an object, and you can’t instantiate it. It has no public, private, or protected access control; code that knows a name can use an otherwise accessible declaration. project::detail states a convention but doesn’t force outside code to stay away.

A namespace isn’t a directory or a build target either. You can extend one namespace across several headers and source files, and one file can contain several namespaces. Projects often keep directory and namespace hierarchies similar for readers, but the language doesn’t require that layout.

You encounter namespaces in nearly every C++ interface. Standard library names are in std; libraries commonly use an organization or library name as the outer namespace and add domain-oriented nesting in larger codebases. C++17 added the compact nested definition syntax namespace company::billing { ... }.

Namespaces solve name ownership and lookup. Header inclusion, the one-definition rule, and whether the linker can find a symbol belong to the compilation and linking model. Keeping those concerns separate is necessary when, for example, code compiles but fails to link.

How it works

A namespace definition adds declarations to a namespace scope. Writing another definition with the same name later reopens the original namespace; it doesn’t create a second container with the same name. A header can therefore declare shop::load(), and a source file can provide its definition inside namespace shop { ... }.

A namespace definition can only appear at namespace scope, not inside a function or class. Inside a function, you may declare a namespace alias or use a using declaration or directive. This distinction catches generated code that tries to create a namespace inside a function for “local isolation”: that code doesn’t compile.

Qualified and unqualified lookup

billing::total is a qualified name, so lookup continues in the scope denoted by billing. A leading :: makes a fully qualified name start at the global namespace. For example, ::company::billing::total doesn’t first try a company declared in the current nested scope.

The unqualified name total uses unqualified name lookup. The compiler searches the current scope and then the outer scopes prescribed by the language. Finding candidate names doesn’t finish a function call; function candidates must still go through overload resolution.

For an unqualified function call, the compiler may also perform argument-dependent lookup (ADL) . Namespaces and classes associated with the argument types contribute more candidates, so print(parcel) can find shipping::print placed alongside shipping::Parcel. The qualified call shipping::print(parcel) doesn’t need ADL.

ADL only supplements particular forms of function lookup; it doesn’t “guess any name from the arguments.” It is also suppressed when ordinary unqualified lookup finds a class member, a block-scope function, or a non-function declaration. When debugging an overload, list ordinary-lookup candidates separately from those supplied by ADL.

using declarations, directives, and aliases

A using declaration introduces a named declaration, as in using metrics::distance;. The current scope can then write distance(...), while the decision remains visible on one line. A function overload set can be introduced under one name, but that doesn’t import the whole namespace.

A using directive has the form using namespace metrics;. It makes unqualified lookup consider names from that namespace; it doesn’t declare a local copy of every member. A later addition to the namespace can turn a formerly unambiguous call into an ambiguous one.

A namespace alias gives a shorter spelling to a long name, as in namespace api = company::platform::api;. It doesn’t copy members or create a new namespace; the alias and original name reach the same declarations. Aliases are useful local conveniences in a .cpp file or function, but public interfaces shouldn’t depend on abbreviations chosen by callers.

FormEffectSuitable scope
metrics::distance(a, b)Qualifies each use explicitlyPublic interfaces, headers, and collision-prone code
using metrics::distanceIntroduces one name or overload setA small function scope
using namespace metricsMakes unqualified lookup consider the namespaceA narrow, unambiguous implementation scope
namespace mt = metricsGives the same namespace a short nameLong qualified paths in local implementation code

Unnamed and inline namespaces

An unnamed namespace is written namespace { ... }. Names declared directly or indirectly within it have internal linkage and belong to the current translation unit . Several unnamed namespace definitions in one translation unit reopen the same unique namespace.

Unnamed namespaces suit helper functions, constants, and implementation types in a .cpp file. In a header, each translation unit that includes the header gets its own entities. That can be intentional, but it can also duplicate state and create type-identity problems.

Members of an inline namespace can also be found by qualified lookup through the enclosing namespace. Libraries often use one to give the current API version a short name while preserving explicit spellings such as library::v1::Type and library::v2::Type. It doesn’t automatically make the two versions source- or ABI-compatible.

Examples

The next four programs show name isolation, namespace extension, ADL, and unnamed and inline namespaces. Each was compiled and run locally with g++ in -std=c++23 mode; the recorded output is from those runs.

Isolating functions with the same name

Both domains can use the name fee. Each call site selects a rule with a qualified name, and a very small scope gives one namespace an alias.

qualified_names.cpp
#include <iomanip>
#include <iostream>

namespace retail {
double fee(double amount) {
    return amount * 0.02;
}
}

namespace wholesale {
double fee(double amount) {
    return amount * 0.01 + 4.0;
}
}

int main() {
    const double order = 250.0;
    namespace bulk = wholesale;

    std::cout << std::fixed << std::setprecision(2);
    std::cout << "retail: " << retail::fee(order) << '\n';
    std::cout << "wholesale: " << bulk::fee(order) << '\n';
}
retail: 5.00
wholesale: 6.50

bulk is only another spelling of wholesale. It has no separate fee and doesn’t change the function’s type or linked symbol. Keeping the calls qualified is clearer here than introducing both functions under the name fee.

Extending a namespace in several places

The first definition adds Order; the second adds make_order(). The C++17 nested syntax then defines shop::audit::write(). All three regions contribute to one connected set of namespace scopes.

namespace_extension.cpp
#include <iostream>

namespace shop {
struct Order {
    int id;
    int units;
};
}

namespace shop {
Order make_order(int id, int units) {
    return {id, units};
}
}

namespace shop::audit {
void write(const Order& order) {
    std::cout << "order " << order.id
              << ": " << order.units << " units\n";
}
}

int main() {
    const auto order = shop::make_order(42, 3);
    shop::audit::write(order);
}
order 42: 3 units

Inside audit, the enclosing shop namespace’s Order can be unqualified. The caller still writes shop::audit::write, because an ordinary nested namespace doesn’t expose its members through the enclosing namespace as an inline namespace does.

A real project usually puts Order and the function declarations in a header and the function definitions in a source file. The sample uses one file so it can run alone. A namespace can span files, but each translation unit still needs declarations for the names it uses.

Letting ADL find an operation from the same domain

shipping::print lives in the same namespace as the Parcel it handles. main() has no using shipping::print, yet the unqualified call can associate the argument type with shipping.

adl_print.cpp
#include <iostream>
#include <string_view>

namespace shipping {
struct Parcel {
    std::string_view route;
    int weight_kg;
};

void print(const Parcel& parcel) {
    std::cout << parcel.route << ": "
              << parcel.weight_kg << " kg\n";
}
}

int main() {
    const shipping::Parcel parcel{"CDG-BER", 12};
    print(parcel);  // ADL adds shipping::print.
}
CDG-BER: 12 kg

If the argument changed to a built-in type unrelated to shipping, ADL wouldn’t have that associated namespace. Placing an operation alongside its user-defined type is what lets operators and many customization points work naturally.

You could write shipping::print(parcel) here instead. Whether to rely on ADL depends on the interface contract: ordinary business calls can usually be qualified, while generic calls designed for customization often need to remain unqualified.

Selecting a default API version

The outer name telemetry::format finds the inline v2 version, while the old version remains explicitly accessible. An unnamed namespace holds a call counter private to this translation unit.

inline_version.cpp
#include <iostream>
#include <string>

namespace telemetry {
namespace v1 {
std::string format(int value) {
    return "value=" + std::to_string(value);
}
}

inline namespace v2 {
std::string format(int value) {
    return "metric:" + std::to_string(value);
}
}
}

namespace {
int calls = 0;
}

int main() {
    ++calls;
    std::cout << telemetry::format(7) << '\n';
    std::cout << telemetry::v1::format(7) << '\n';
    std::cout << "calls: " << calls << '\n';
}
metric:7
value=7
calls: 1

telemetry::v2::format(7) is also valid. Here inline changes lookup and some association rules; it doesn’t carry the function-optimization meaning of the other use of the same keyword.

calls isn’t a member of telemetry. It provides an internal entity only in this translation unit, which fits an implementation detail. Code that needs one counter across files should declare an explicit external interface in a named namespace.

Pitfalls

Writing using namespace in a header

Fix: use qualified names in headers. If repeated qualification genuinely harms readability inside an implementation function, introduce only the needed name in the smallest practical block scope.

Treating detail as access control

Fix: use private class members for object invariants, an unnamed namespace for .cpp-local names, and module export rules for module interfaces. Don’t treat a namespace name as a security boundary.

Creating one state object per translation unit in a header

Fix: first state whether ownership is per translation unit or program-wide. Put shared state behind one definition in a suitable namespace, or use a C++17 inline variable when its contract permits. Document independent state when that behavior is intentional.

Adding your own declarations to std

Fix: put a type and its non-member operations in your own namespace so ADL can find them. For generic swapping, introduce std::swap and then call swap(a, b) unqualified, or use std::ranges::swap when that matches the target library contract.

Letting a qualified call bypass ADL customization

Fix: confirm whether an API defines ADL as part of its customization protocol. Follow that protocol with an unqualified call and test at least one custom type. Prefer qualification for ordinary calls to an operation owned by a known namespace.

Defining a function in the wrong namespace

Fix: define the function with an explicit qualified name or open exactly the same namespace as its declaration. Compile and link a minimal caller. Running only -fsyntax-only on the source file can’t detect a missing definition.

Deep Translation units, headers, and linkage

Translation units, headers, and linkage

A translation unit is one source file after preprocessing, including the content brought in from headers. Compilers usually compile translation units separately before the linker resolves entities with the appropriate linkage. A namespace scope can continue across translation units, but one unit doesn’t automatically see declarations from another file.

A named namespace doesn’t make its members an “exported API,” nor does it give every member the same linkage. Functions, variables, templates, const objects, and inline entities still follow their respective declaration rules. An interface design must answer where a name belongs, who sees its declaration, and how many definitions exist.

Headers usually provide declarations in a named namespace. One source file provides the definition of an ordinary non-inline function in that same namespace. Functions or variables defined in several translation units need a language mechanism that permits matching definitions, and they must still satisfy the one-definition rule.

An unnamed namespace gives its names internal linkage, but it doesn’t make a header run only once for the whole program. Each translation unit that includes the header receives a definition in that unit after preprocessing. State copies, address comparisons, and code that depends on type identity can all become surprising.

Namespaces and C++20 modules can be used together. A namespace organizes names and participates in lookup; a module controls exported declarations and visibility across module units. Moving code into a module doesn’t remove name collisions, and putting it in a namespace doesn’t create a module visibility boundary.

SymptomCheck first
Compilation succeeds but linking reports an undefined referenceWhether declaration and definition have the same fully qualified name
A call becomes ambiguous after adding a headerNew declarations, using directives, and ADL candidates
Two source files observe different countersWhether a header created internal-linkage entities
Outside code calls a detail memberWhether code mistook a naming convention for access control

The global namespace and explicit qualification

Every named namespace is ultimately nested in the global namespace. The global namespace has no name you can write in a declaration, but a leading :: explicitly starts lookup there. Ordinary library code rarely needs this form; it occasionally helps in templates or deeply nested scopes with heavy shadowing.

Both ::name and namespace_name::name use qualified lookup, but they start differently. The first starts only at global scope; the second must first resolve the namespace on its left. If the current scope also declares a type or variable called company, ::company::api can rule out that shadowing.

A leading :: doesn’t “invoke the linker” and doesn’t change an entity’s linkage. It only constrains source-level name lookup. Linking still depends on a matching definition and on the build supplying the corresponding object file or library.

Business names in the global namespace are the easiest to collide. C-compatible entry points, main, and some platform interfaces may have to remain global. Most other project declarations belong in a stable outer namespace.

Namespace ownership in public APIs

The outermost namespace usually represents a long-lived library or organization identity. Names such as utils, common, and core collide easily with dependencies and don’t identify an owner. A name that stays clear when several libraries are composed works better than a prefix on every function.

Nesting should express conceptual ownership instead of mechanically copying a directory tree. Directories move as builds and teams change, while a public type’s fully qualified name appears in client source, diagnostics, documentation, and many ABI symbols. Each added level should carry a stable meaning that readers can explain.

Parameter and return types carry namespace names into public signatures. Even if a function is exposed through an alias, callers may still see the original type in annotations, specializations, and diagnostics. A namespace alias can help a migration, but it can’t pretend that a type belongs to another namespace.

Renaming a public namespace is normally a source-breaking change and may alter symbols under common ABIs. A migration can temporarily provide aliases or forwarding declarations, but it must test ADL, specialization placement, and ambiguities when both versions appear. A text replacement alone doesn’t prove compatibility.

A useful interface test includes the library and an unrelated library with common names in one file, with no global using directive, then compiles real calls. This exposes leaked global names, over-broad directives, and dependencies on header order early.

using timing and scope

A namespace using declaration introduces declarations found by qualified lookup at that point. If the source namespace later gains another function overload with the same name, the earlier using declaration usually doesn’t acquire it automatically. Rules such as partial specialization have specific exceptions, so “always a snapshot” is too broad for every entity.

A using directive behaves differently. It makes unqualified lookup consider the nominated namespace, so declarations added there later can affect calls. In a large header, the real problem isn’t saving a few qualifiers; it’s that the candidate set can shift with include order and library evolution.

A block-scope using declaration affects that block and the nested scopes prescribed by the language. Putting it next to repeated calls inside one function limits how far a reader must trace name ownership. A namespace-scope using reaches farther and deserves the same review as an interface declaration.

A namespace alias also obeys scope. It binds to an existing namespace, can’t be “reopened” like the original namespace, and can’t be changed to another target in the same scope. Add members through a definition of the original namespace, not by trying to use the alias as a new namespace definition.

ADL, hidden friends, and customization points

ADL derives its associated set from function argument types. A class type contributes associated classes and namespaces, and template arguments can expand the set; fundamental types don’t supply an associated namespace. The exact rules are finer than “look in the arguments’ namespaces,” so check complex overloads against compiler diagnostics and the standard.

A hidden friend is a non-member friend function first declared and defined inside a class definition. Ordinary qualified lookup may not find it, while a call with that class as an argument can find it through ADL. Comparison operators often use this form because the function should enter the candidate set only when at least one operand has that type.

This mechanism reduces unrelated candidates, but a careless refactor can break it. A model may rewrite an unqualified expression as namespace_name::operator==(...) without noticing that the hidden friend can’t be found that way. Test such changes with the actual operator expression or agreed customization-point call instead of only searching for the function spelling.

The classic swap protocol introduces std::swap as a fallback candidate in the current scope and then makes an unqualified call. Ordinary types can use the standard implementation, while ADL can select a better operation placed alongside a user-defined type. Don’t simulate the protocol by adding ordinary overloads to std.

Inline namespaces also participate in ADL association. If the associated set contains an inline namespace, its enclosing namespace is added; if it contains the enclosing namespace, its inline namespaces are added. This lets versioned types cooperate with outer customization operations, but it can also widen the candidate set.

Inline namespaces and version boundaries

An inline namespace lets the current version’s member be written library::Widget while retaining the explicit name library::v2::Widget. Put the old version in an ordinary nested namespace, and clients must write library::v1::Widget. Even with the same class name, the two types are members of different namespaces.

Common ABIs encode namespace levels in symbol names, so version namespaces can let symbols for separate implementations coexist. The C++ language standard doesn’t prescribe one name-mangling format, however. Binary compatibility still depends on the target ABI, object layout, calling conventions, and release policy.

Changing which version is inline changes the API found through the outer name. It can select a default in a controlled release, but it isn’t a cost-free upgrade switch. Recompiling source, loading old binaries, and interoperating between objects from two versions are separate problems and need separate tests.

A version namespace shouldn’t replace a clear deprecation policy. If an old version remains supported, document how long its headers, libraries, and symbols remain. When it is removed, build or link failures should be an expected migration signal, not an accidental result of overload resolution.

Further reading

checkpoint

4 questions · 1 predict-the-output · 1 spot-the-bug

before this C fundamentals
next up Classes Cmake soon Operator overloading Templates soon
Copy as Markdown Interview bank Edit on GitHub Report an error Was this clear?