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 blocks, and conditional compilation lets you target 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.
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: 80Everything inside namespace audio { ... } is accessed with the audio:: prefix.
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();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++.
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().
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 |
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_HThe 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.
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.
#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 possibleFunction-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)) = 25The 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.
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>
#endifCommon 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
#endifWhen 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.
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
Basic Syntax
A module is defined with export module:
// greeting.cppm (module interface file)
export module greeting;
import <string>;
export std::string greet(const std::string& name)
{
return "Hola, " + name + "!";
}And consumed with import:
// main.cpp
import greeting;
import <iostream>;
int main()
{
std::cout << greet("Mundo") << "\n";
return 0;
}Current State
Module support is improving across compilers, but as of this writing: - MSVC has the most mature support - GCC and Clang support modules but with some limitations - Build system support (CMake, etc.) is still evolving
Tip: Modules are the future of C++ code organization, but the ecosystem is not fully there yet. Learn the concepts now, and start using them when your toolchain supports them well. For now, headers with #pragma once remain the practical choice for most projects.
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.
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
staticat file scope. - Inline namespaces make their contents accessible as if they were in the enclosing namespace, useful for API versioning.
- A
usingdeclaration imports a single name; ausingdirective imports an entire namespace. Never useusing namespacein header files. - Include guards (
#ifndef/#define/#endif) or#pragma onceprevent double-inclusion of headers. - Macros are text substitution. Prefer
constexprfor constants and templates/inlinefor 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. Ecosystem support is still maturing.
Exercises
Think about it: Why is
using namespace std;in a header file dangerous? Give a specific example of a name collision it could cause.What does this print?
namespace a { int x = 1; namespace b { int x = 2; } } std::cout << a::x << " " << a::b::x << "\n";Where is the bug?
// file1.cpp namespace { int count = 0; } // file2.cpp extern int count; void increment() { count++; }Think about it: When would you use an inline namespace? Describe a real-world scenario where it would be useful.
What does this print?
#define DOUBLE(x) x * 2 int result = DOUBLE(3 + 4); std::cout << result << "\n";Where is the bug? (And what is the fix?)
// utils.h using namespace std; string format(int x);Think about it: What advantages do C++20 modules have over traditional headers? Why hasn’t the industry fully adopted them yet?
What does this print?
namespace outer { inline namespace inner { int value = 42; } } std::cout << outer::value << "\n"; std::cout << outer::inner::value << "\n";Calculation: Given the macro:
#define MAX(a, b) ((a) > (b) ? (a) : (b))What is the value of
MAX(3+1, 2+3)?Write a program that defines a namespace
musicwith sub-namespacesrockandpop, each containing a functiontop_song()that returns a different string. Use ausingdeclaration to bring one into scope and call both. Add an anonymous namespace with a helper function used by both sub-namespaces.