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

  • Namespace = named scope. 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 inside accessed via namespace::name.
  • Same namespace reopenable across TUs — declarations merge.
// math.h
namespace math {
    double area(double r);
}

// shapes.h
namespace math {                  // reopens math, adds another declaration
    double perimeter(double r);
}
  • Global namespace = unnamed. 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

  • inline namespace's members visible in enclosing namespace as if 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
  • Library authors evolve ABI without breaking source compat — switch which version is inline to change default while keeping older versions accessible.

3. using Declarations, Directives, and Aliases

  • 3 using constructs:

using declaration

  • Single name into current scope.
using std::cout;
using std::endl;

cout << "hi" << endl;

using directive

  • All names from a namespace into current scope. Convenient but pollutes; 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 inherits the dump → surprising collisions.

Namespace alias

  • Shortens long namespace names within a scope.
namespace fs = std::filesystem;
fs::path p = "/tmp";

Type alias

  • using (C++11) replaces typedef; cleaner with 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 (a.k.a. Koenig lookup) — rule that lets operator<< etc. work without qualification.

Unqualified function call → compiler looks in:

  1. Usual scopes — current + enclosing.
  2. Argument types' namespaces — 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 → would need ns::inspect(w) explicitly.
  • ADL makes range-based for, swap, 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
}
  • Two-step pattern (using std::swap; then unqualified swap) — canonical way to invoke user-customizable swap via ADL.
  • ADL doesn't apply to fundamental-type args (empty associated namespace set) → bare swap(v[0], v[1]) on vector<int> won't find std::swap without qualification or two-step.

Why this is sometimes surprising

  • ADL searches all arg-type namespaces → 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
  • Generally a feature (ADL = customization point), but can cause unexpected overload picking.
  • stdlib uses ADL deliberately for std::swap, std::begin, std::end, <<, etc.

5. The Hidden Friend Idiom

  • "Hidden friend" = friend function defined inside a class body.
  • Only findable by ADL — not by ordinary name lookup.
  • Modern idiom for 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 it.
  4. No template <typename T> boilerplate.
  • Use for binary operators (+, ==, <<, <=>) of value types.

6. friend Functions and Classes

  • friend grants access to private/protected members of a class to a specific function or class outside 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 needing private state (esp. <<, ==).
  2. Tightly-coupled "buddy" classes — one is genuinely impl detail of the other (iterator + container).
  3. Hidden friend idiom (see § 5).

When NOT to use friend

  1. Workaround for poor encapsulation. Many classes needing access → wrong design; expose proper public interface.
  2. Avoiding accessor methods. Just write the accessors.
  • friend is not transitive (B's friend ≠ A's friend) and not inherited (friend of Base ≠ friend of Derived).
  • Grants narrow, deliberate access.