8 Namespaces and the Preprocessor

As projects grow, name collisions become inevitable. Two libraries might both define a log function, or your Error class might clash with one from a dependency. Namespaces solve this by grouping names into distinct scopes. The preprocessor, inherited from C, controls what code the compiler sees — include guards prevent double-inclusion, macros define constants, and conditional compilation selects code blocks for different platforms. C++20 modules aim to replace much of the preprocessor’s job. In this chapter you will learn namespace design, the preprocessor’s key features, and a preview of modules.

8.1 Namespace Basics

You have used std:: since the beginning of Gorgo Starting C++. std is a namespace — a named scope that contains the entire standard library. You can create your own:

#include <iostream>

namespace audio {

void play(const char* track) {
    std::cout << "Playing: " << track << "\n";
}

int volume = 80;

}  // namespace audio

int main() {
    audio::play("Yellow");
    std::cout << "Volume: " << audio::volume << "\n";

    return 0;
}
Playing: Yellow
Volume: 80

Everything inside namespace audio { ... } is accessed with the audio:: prefix.

8.1.1 Nested Namespaces

Before C++17, nesting namespaces required separate blocks:

namespace company {
    namespace audio {
        namespace codec {
            void decode() {}
        }
    }
}

C++17 lets you write this in one line:

namespace company::audio::codec {
    void decode() {}
}

company::audio::codec::decode();

8.1.2 Namespace Aliases

Fully qualified names like company::audio::codec get tedious to type. A namespace alias introduces a shorter name for an existing namespace:

namespace fs = std::filesystem;

Now fs::path means std::filesystem::path. The alias is scoped like any other declaration, so one .cpp file (or one function) can shorten a long name without affecting any other code. You will see this exact alias again in Chapter 11.

8.2 Anonymous Namespaces

An anonymous namespace gives its contents internal linkage — they are visible only within the current file:

namespace {
    int helper_count = 0;

    void internal_helper() {
        helper_count++;
    }
}

This is the modern C++ replacement for the static keyword at file scope. Other files cannot see helper_count or internal_helper even if they try to declare them with extern.

Tip: Use anonymous namespaces instead of static for file-local functions and variables. The static keyword at file scope is a holdover from C and is considered less idiomatic in C++.

8.3 Inline Namespaces

Inline namespaces make their contents accessible as if they were in the enclosing namespace. They are primarily used for API versioning:

#include <iostream>

namespace mylib {

inline namespace v2 {
    void greet() { std::cout << "Hola from v2!\n"; }
}

namespace v1 {
    void greet() { std::cout << "Hola from v1!\n"; }
}

}  // namespace mylib

int main() {
    mylib::greet();       // calls v2::greet (inline namespace)
    mylib::v1::greet();   // explicitly calls v1
    mylib::v2::greet();   // explicitly calls v2

    return 0;
}
Hola from v2!
Hola from v1!
Hola from v2!

By marking v2 as inline, users who call mylib::greet() automatically get the latest version. Users who need the old version can explicitly qualify mylib::v1::greet().

8.4 Argument-Dependent Lookup (ADL)

When you write std::cout << "hi", the compiler does not require you to write std::operator<<(std::cout, "hi"). The unqualified << works because of argument-dependent lookup (ADL), sometimes called Koenig lookup after Andrew Koenig.

The rule: when the compiler looks up an unqualified function name, it also searches the namespaces of the types of the arguments. For std::cout << "hi", the left operand has type std::ostream, which lives in std, so the compiler also searches std and finds std::operator<< defined there.

#include <iostream>
#include <string>

namespace audio {
    struct Track {
        std::string title;
    };

    // operator<< lives in `audio`, not in the global namespace
    std::ostream& operator<<(std::ostream& os, const Track& t) {
        return os << "[" << t.title << "]";
    }
}

int main() {
    audio::Track t{"Toxic"};
    std::cout << t << "\n";       // ADL finds audio::operator<<
    return 0;
}

You did not have to write audio::operator<<(std::cout, t) — ADL noticed that t has type audio::Track and looked inside audio for an operator<<.

ADL is also why the standard library tells you to write swaps as:

using std::swap;
swap(a, b);              // not std::swap(a, b)

The using line introduces std::swap as a candidate, then unqualified swap(a, b) lets ADL pull in any user-defined swap from the namespace of a’s type. The user-defined version, if it exists, is preferred; otherwise the standard fallback wins. Writing std::swap(a, b) directly skips ADL and forces the standard fallback even when the user provided a better one.

Tip: ADL is what makes std::cout << my_type work when my_type is in a different namespace. Define operator<< (and other operators) in the same namespace as the type, not in the global namespace — that way ADL can find it.

Wut: ADL works for free functions and operators, not for member functions. A t.print() call always uses ordinary lookup; no namespace-of-the-arguments search happens.

8.4.1 Hidden Friends

A hidden friend is a friend function defined inside the class body. Unlike an ordinary friend (which is just a declaration), a hidden friend has its body right there:

namespace audio {
    class Track {
        std::string title_;
        int         year_;
    public:
        Track(std::string title, int year)
            : title_(std::move(title)), year_(year) {}

        // hidden friend: defined inline, found only via ADL
        friend std::ostream& operator<<(std::ostream& os, const Track& t) {
            return os << t.title_ << " (" << t.year_ << ")";
        }

        friend bool operator==(const Track&, const Track&) = default;
    };
}

The function is not visible to ordinary name lookup. You cannot find it by typing audio::operator<< — it only appears when ADL goes hunting through audio because one of the arguments has type audio::Track. That sounds like a downside, but it is a feature: every namespace your code touches no longer pays a lookup cost for operator<< overloads it does not care about.

In big codebases (large standard libraries, anything Boost-shaped), the hidden-friend idiom dramatically improves compile times because overload resolution has fewer candidates to consider for every << call. It is also the modern, ADL-friendly way to expose binary operators that need access to private members.

Compare to the older two-step form:

class Track {
    // ...
    friend std::ostream& operator<<(std::ostream&, const Track&);
};

std::ostream& operator<<(std::ostream& os, const Track& t) {
    return os << t.title_ << " (" << t.year_ << ")";
}

Both compile. Both produce the same calls. But the older form pollutes the surrounding namespace with the operator<< declaration, while the hidden-friend form keeps it tucked inside the class.

Tip: Default to the hidden-friend form for operator<<, operator==, operator<=>, and other binary operators that need private access. You get the same private access as a traditional friend, with better lookup hygiene.

8.5 using Declarations vs. using Directives

There are two ways to bring namespace members into the current scope.

A using declaration imports a single name:

using std::cout;
using std::string;

cout << "No prefix needed\n";
string s = "clean";

A using directive imports an entire namespace:

using namespace std;

cout << "Everything from std is visible\n";

Trap: Never put using namespace std; (or any using directive) in a header file. It pollutes the namespace of every file that includes the header, causing unexpected name collisions. using namespace in a .cpp file or inside a function is acceptable but use it with care.

The guideline:

Context Recommendation
Header files Never use using namespace. Use full qualification.
Source files (top level) using declarations for frequently used names
Inside functions using namespace is acceptable for convenience

8.6 Include Guards

When a header file is #included from multiple places, the compiler can see the same declarations twice. Include guards prevent this:

// audio.h --- traditional include guard
#ifndef AUDIO_H
#define AUDIO_H

void play(const char* track);

#endif  // AUDIO_H

The first time audio.h is included, AUDIO_H is not defined, so the content is processed and AUDIO_H gets defined. The second time, AUDIO_H is already defined, so the entire content is skipped.

#pragma once is a simpler alternative supported by all major compilers:

// audio.h --- pragma once
#pragma once

void play(const char* track);
Feature Include guards #pragma once
Standard Yes (part of the language) Not standard, but universally supported
Syntax Verbose (3 lines) One line
Edge cases Works everywhere May fail with symlinks or network drives

Tip: Either approach works. #pragma once is simpler and is the de facto standard in modern codebases. Use traditional include guards if your build environment has exotic filesystem issues.

8.7 Macros and Conditional Compilation

The C preprocessor runs before the compiler sees your code. Macros are text substitutions — they replace one sequence of tokens with another.

8.7.1 #define

#define MAX_TRACKS 100
#define PI 3.14159

int tracks[MAX_TRACKS];

The preprocessor replaces every occurrence of MAX_TRACKS with 100 before compilation.

Tip: Prefer constexpr variables over #define for constants. constexpr is type-safe and respects scopes; macros do not:

constexpr int max_tracks = 100;   // preferred
#define MAX_TRACKS 100            // avoid when possible

8.7.2 Function-Like Macros

#define SQUARE(x) ((x) * (x))

int a = SQUARE(5);    // expands to ((5) * (5)) = 25
int b = SQUARE(2+3);  // expands to ((2+3) * (2+3)) = 25

The extra parentheses are critical — without them, SQUARE(2+3) would expand to 2+3 * 2+3 = 11.

Trap: Macros are pure text replacement. They do not understand types, scopes, or expressions. Prefer constexpr functions or templates over function-like macros.

8.7.3 Conditional Compilation

Conditional compilation lets you include or exclude code based on compile-time conditions:

#ifdef _WIN32
    #include <windows.h>
#elif defined(__linux__)
    #include <unistd.h>
#elif defined(__APPLE__)
    #include <mach/mach.h>
#endif

Common predefined macros:

Macro Meaning
_WIN32 Windows
__linux__ Linux
__APPLE__ macOS / iOS
__cplusplus C++ standard version (e.g., 202302L for C++23)
NDEBUG Release mode (disables assert)
#if __cplusplus >= 202002L
    // C++20 or later
    #include <ranges>
#else
    // Fallback for older compilers
#endif

8.7.4 When to Use Macros

Macros still have legitimate uses:

  • Include guards
  • Platform-specific conditional compilation
  • assert() (needs to capture __FILE__ and __LINE__)
  • Compile-time feature detection

For everything else — constants, inline functions, type-safe generics — use constexpr, inline, and templates.

8.8 Modules Preview (C++20)

C++20 introduced modules as a modern replacement for the #include / header-file model. Modules solve several long-standing problems:

  • Headers are processed every time they are included, slowing compilation
  • Include order can matter (macros leak across headers)
  • Include guards / #pragma once are workarounds, not solutions

8.8.1 Basic Syntax

A module is defined with export module:

// greeting.cppm (module interface file)
export module greeting;

import std;

export std::string greet(const std::string& name) {
    return "Hola, " + name + "!";
}

And consumed with import:

// main.cpp
import std;
import greeting;

int main() {
    std::cout << greet("Mundo") << "\n";

    return 0;
}

import std; (C++23) makes the entire standard library available in one line — no #include list and no header order to worry about.

8.8.2 Building Modules

Building modules takes more ceremony than a plain g++ main.cpp, because every import must be compiled before the code that imports it. With GCC, modules are enabled by the -fmodules flag, and the compiled module files land in a gcm.cache/ directory:

# compile the std module shipped with libstdc++ (once per build directory)
g++ -std=c++23 -fmodules -fsearch-include-path -c bits/std.cc

# compile the module interface before the file that imports it
g++ -std=c++23 -fmodules greeting.cppm main.cpp std.o -o hello
./hello
Hola, Mundo!

8.8.3 Current State

Module support has matured across the ecosystem, but as of this writing:

  • MSVC, GCC (15 and later), and Clang all support named modules and import std; (C++23)
  • CMake has official support for named modules since version 3.28
  • Adoption is gradual — most existing libraries still ship headers, so real projects mix modules and #include

Tip: Modules are the future of C++ code organization, and the major toolchains now support them. Learn the concepts now, and reach for them in new projects with a recent compiler and CMake 3.28+. Headers with #pragma once remain the practical choice when you depend on header-only libraries or older toolchains.

8.9 Try It: Namespace Organization

Here is a program that demonstrates namespace design. Type it in, compile with g++ -std=c++23, and experiment:

#include <iostream>
#include <string>

namespace studio {

namespace audio {
    void play(const std::string& track) {
        std::cout << "Playing: " << track << "\n";
    }
}

namespace video {
    void play(const std::string& clip) {
        std::cout << "Showing: " << clip << "\n";
    }
}

inline namespace v2 {
    std::string format_title(const std::string& title) {
        return "[" + title + "]";
    }
}

namespace v1 {
    std::string format_title(const std::string& title) {
        return title;
    }
}

}  // namespace studio

namespace {
    int internal_counter = 0;
    void tick() { internal_counter++; }
}

int main() {
    // Nested namespaces
    studio::audio::play("Speed of Sound");
    studio::video::play("music video");

    // Inline namespace (v2 is default)
    std::cout << studio::format_title("Harder Better Faster Stronger") << "\n";
    std::cout << studio::v1::format_title("Harder Better Faster Stronger") << "\n";

    // Anonymous namespace
    tick();
    tick();
    std::cout << "Counter: " << internal_counter << "\n";

    // using declaration
    using studio::audio::play;
    play("Around the World");

    // Conditional compilation
    #ifdef NDEBUG
        std::cout << "Release build\n";
    #else
        std::cout << "Debug build\n";
    #endif

    return 0;
}

Try renaming the play functions to the same name in different namespaces and see how the compiler resolves them. Try removing the inline from v2 and see what happens when you call studio::format_title.

8.10 Key Points

  • Namespaces group names to avoid collisions. Use :: to access members.
  • Nested namespaces can be declared with namespace a::b::c { } (C++17).
  • Anonymous namespaces give contents internal linkage (file-local visibility), replacing static at file scope.
  • Inline namespaces make their contents accessible as if they were in the enclosing namespace, useful for API versioning.
  • A using declaration imports a single name; a using directive imports an entire namespace. Never use using namespace in header files.
  • Include guards (#ifndef/#define/#endif) or #pragma once prevent double-inclusion of headers.
  • Macros are text substitution. Prefer constexpr for constants and templates/inline for function-like macros.
  • Conditional compilation (#ifdef, #if) is useful for platform-specific code and feature detection.
  • Modules (C++20) are the modern alternative to headers, offering faster compilation and better isolation. Compiler and build-system support is in place; library adoption is still ramping up.

8.11 Exercises

  1. Think about it: Why is using namespace std; in a header file dangerous? Give a specific example of a name collision it could cause.

  2. What does this print?

    namespace a {
        int x = 1;
        namespace b {
            int x = 2;
        }
    }
    
    std::cout << a::x << " " << a::b::x << "\n";
  3. Where is the bug?

    // file1.cpp
    namespace { int count = 0; }
    
    // file2.cpp
    extern int count;
    void increment() { count++; }
  4. Think about it: When would you use an inline namespace? Describe a real-world scenario where it would be useful.

  5. What does this print?

    #define DOUBLE(x) x * 2
    
    int result = DOUBLE(3 + 4);
    std::cout << result << "\n";
  6. Where is the bug? (And what is the fix?)

    // utils.h
    using namespace std;
    
    string format(int x);
  7. Think about it: What advantages do C++20 modules have over traditional headers? Why hasn’t the industry fully adopted them yet?

  8. What does this print?

    namespace outer {
        inline namespace inner {
            int value = 42;
        }
    }
    
    std::cout << outer::value << "\n";
    std::cout << outer::inner::value << "\n";
  9. Calculation: Given the macro:

    #define MAX(a, b) ((a) > (b) ? (a) : (b))

    What is the value of MAX(3+1, 2+3)?

  10. Write a program that defines a namespace music with sub-namespaces rock and pop, each containing a function top_song() that returns a different string. Use a using declaration to bring one into scope and call both. Add an anonymous namespace with a helper function used by both sub-namespaces.

  11. What does this print, and why does it compile?

    #include <iostream>
    #include <string>
    
    namespace mylib {
        struct Track { std::string title; };
    
        std::ostream& operator<<(std::ostream& os, const Track& t) {
            return os << "<" << t.title << ">";
        }
    }
    
    int main() {
        mylib::Track t{"Mr. Brightside"};
        std::cout << t << "\n";
        return 0;
    }

    Where does the compiler find operator<<? What is the name of the lookup rule that makes the call work without mylib::operator<<(std::cout, t)?

  12. Where is the bug?

    #include <iostream>
    #include <string>
    
    struct Loud {
        std::string s;
    };
    
    namespace { std::ostream& operator<<(std::ostream& os, const Loud& l) {
        return os << l.s;
    } }
    
    int main() {
        Loud x{"BEYONCE"};
        std::cout << x << "\n";
        return 0;
    }

    The program compiles — but if you move the operator<< definition into a namespace silent { ... } instead of the anonymous namespace, the call no longer compiles. Explain the difference in terms of ADL.

  13. Think about it: In a 100-file project, every translation unit that does any output has to consider every visible operator<< overload during overload resolution. Why might converting operator<< definitions from “free function in namespace” to “hidden friend in the class” measurably speed up compilation?