C++ Namespaces and Name Lookup


  • Description: A note on namespaces, using directives and declarations, namespace aliases, argument-dependent lookup (ADL), the hidden friend idiom, and friend functions and classes
  • My Notion Note ID: K2A-B1-3
  • Created: 2018-09-16
  • Updated: 2026-02-28
  • License: Reuse is very welcome. Please credit Yu Zhang and link back to the original on yuzhang.io

Table of Contents


1. Namespaces Basics

A namespace is a named scope that prevents identifier collisions between libraries.

namespace math {
    constexpr double pi = 3.14159265358979;
    double area(double r) { return pi * r * r; }
}

double a = math::area(5.0);   // qualified access

Names declared inside a namespace are accessed with namespace::name. The same namespace can be reopened multiple times across TUs — declarations from each are merged.

// math.h
namespace math {
    double area(double r);
}

// shapes.h
namespace math {                  // reopens math, adds another declaration
    double perimeter(double r);
}

The global namespace has no name; you can refer to it explicitly with ::name:

int x = 1;
namespace ns {
    int x = 2;
    int y = ::x;       // 1 (global x)
    int z = x;         // 2 (ns::x)
}

2. Nested and Inline Namespaces

// Nested namespaces (C++17 syntax — older syntax also works)
namespace company::project::util {
    void helper();
}

// Older equivalent:
namespace company { namespace project { namespace util {
    void helper();
}}}

// Use:
company::project::util::helper();

Inline namespaces

An inline namespace's members are visible in the enclosing namespace as if they were declared there directly. Common use: ABI versioning.

namespace mylib {
    inline namespace v2 {
        void api();              // looks like mylib::api() to users
    }
    namespace v1 {
        void api();              // accessed only as mylib::v1::api()
    }
}

mylib::api();          // calls v2::api()
mylib::v1::api();      // explicit old version

Inline namespaces let library authors evolve an ABI without breaking source compatibility — switch which version is inline to change the default while keeping older versions accessible.


3. using Declarations, Directives, and Aliases

Three different using constructs:

using declaration

Brings a single name into the current scope.

using std::cout;
using std::endl;

cout << "hi" << endl;

using directive

Brings all names from a namespace into the current scope. Convenient but pollutes the scope; avoid in headers.

using namespace std;        // OK in a .cpp at function scope; AVOID in headers

void f() {
    cout << "hi";           // works
}

Never write using namespace std; in a header. Every TU that includes it inherits the dump, causing surprising name collisions.

Namespace alias

Shortens a long namespace name within a scope.

namespace fs = std::filesystem;
fs::path p = "/tmp";

Type alias

using (C++11) replaces typedef for type aliases, with cleaner syntax for templates:

using IntPtr = int*;                              // same as: typedef int* IntPtr;
using Callback = std::function<void(int)>;        // function-pointer-like alias

template <typename T>
using Vec = std::vector<T>;                        // alias template
Vec<int> v;

4. Argument-Dependent Lookup (ADL)

ADL (also called Koenig lookup) is the rule that lets operator<< and friends work without qualification.

When you call an unqualified function, the compiler looks for it in:

  1. The usual scopes (current scope, enclosing scopes).
  2. The namespaces of the argument types — and friends declared inside their classes.
namespace ns {
    struct Widget {};
    void inspect(Widget) {}
}

ns::Widget w;
inspect(w);      // OK: ADL finds ns::inspect because w is in ns

Without ADL, you'd have to write ns::inspect(w) explicitly. ADL is what makes range-based for, swap, and stream operators work cleanly across namespaces:

template <typename T>
void shuffle(T& a, T& b) {
    using std::swap;       // bring std::swap into scope
    swap(a, b);            // ADL picks T's swap if defined; falls back to std::swap
}

The "two-step" pattern (using std::swap; then unqualified swap) is the canonical way to invoke a swap that an end-user can customize for their own type via ADL. Note that ADL does not apply to fundamental-type arguments (their associated namespace set is empty), so a bare swap(v[0], v[1]) on a vector<int> would not find std::swap — you'd need either qualification or the two-step idiom.

Why this is sometimes surprising

ADL searches all argument-type namespaces, which can match more than intended:

namespace ns2 {
    struct Marker {};
    void process(int, Marker) {}
}

ns2::Marker m;
process(0, m);   // ADL finds ns2::process via m's namespace

This is generally a feature (ADL = customization point), but it can cause unexpected overload picking. The standard library uses ADL deliberately for std::swap, std::begin, std::end, <<, etc.


5. The Hidden Friend Idiom

A "hidden friend" is a friend function defined inside a class body. It's only findable by ADL — not by ordinary name lookup. This is the modern idiomatic way to define non-member operators on a class:

class Money {
    int cents_;
public:
    explicit Money(int c) : cents_(c) {}

    // Hidden friend — defined inside the class body
    friend Money operator+(Money a, Money b) {
        return Money(a.cents_ + b.cents_);
    }

    friend bool operator==(const Money& a, const Money& b) {
        return a.cents_ == b.cents_;
    }
};

Money x{100}, y{200};
auto z = x + y;             // calls hidden friend via ADL

Why hidden friends are good:

  1. No template instantiation pollution — they're not function templates.
  2. Better overload-resolution behavior — won't match unrelated calls.
  3. Clear association with the class — definition lives next to the class.
  4. Avoids the template <typename T> boilerplate — no need to be a template.

Use hidden friends for binary operators (+, ==, <<, <=>) of value types.


6. friend Functions and Classes

friend declarations grant access to private and protected members of a class to a specific function or class outside of it.

class Account {
    double balance_;
    friend class AuditLog;                   // class friend
    friend void freeze(Account&);            // function friend
    friend bool operator==(const Account&, const Account&) = default;
};

class AuditLog {
public:
    void check(Account& a) { a.balance_; }   // OK: AuditLog is a friend
};

void freeze(Account& a) { a.balance_ = 0; }  // OK: friend function

When to use friend

  1. Operator overloads that need private state (especially << and ==).
  2. Tightly-coupled "buddy" classes where one is genuinely an implementation detail of the other (e.g., iterator + container).
  3. The hidden friend idiom (see § 5).

When NOT to use friend

  1. As a workaround for poor encapsulation. If many classes need access, the design is wrong; expose a proper public interface.
  2. To avoid writing accessor methods. Just write the accessors.

friend is not transitive (B's friend isn't A's friend) and is not inherited (a friend of Base is not a friend of Derived). Friendship grants narrow, deliberate access.