Creating Custom Namespaces
Learn how to organize code with user-defined namespaces and the scope resolution operator to avoid naming conflicts.
What Is a User-Defined Namespace?
A namespace is a named region that takes ownership of everything declared inside it. Wrap a function called weight() in namespace Freight and the function is no longer called weight at all: its full name becomes Freight::weight, and code elsewhere has to spell it that way. A user-defined namespace is one you wrote yourself, as opposed to one that arrived with the standard library or a vendor's headers. Some people prefer the term program-defined namespace, which describes it more exactly, but the older label is what you will see in most codebases.
The :: sitting between the two halves is the scope resolution operator. It settles the question of where a name should be looked up: whatever stands to its left is the region to search, and whatever stands to its right is the name being searched for.
This lesson is about what changes once your names move into regions of your own, and how to reach them afterwards.
The Problem Namespaces Solve
You met naming collisions back in the lesson on naming collisions and namespaces: two identical names arrive in one scope, nothing in the language says which of them wins, and the build stops. Namespaces exist because that situation gets more likely the more successful your project becomes.
Adding one name to a scope does not add one chance of a clash. The newcomer can collide with everything already sitting there, so the number of pairs that could go wrong climbs far faster than the list of names does. That arithmetic is the whole argument for keeping every name in the tightest region it can live in.
Here is the collision in its most ordinary form. Two teams contribute to one program, and each ships a file with a function that totals up a shipment. Both picked the obvious name.
freight_lib.cpp:
int weight(int crates, int perCrate)
{
return crates * perCrate;
}
postal_lib.cpp:
int weight(int crates, int perCrate)
{
return crates * perCrate + crates;
}
The third file wants a total and forward declares what it expects to find. Taken together these three files do not compile into a working program, and the reason is worth spelling out.
main.cpp:
#include <iostream>
int weight(int crates, int perCrate);
int main()
{
std::cout << weight(7, 12) << '\n';
return 0;
}
Build either library file with main.cpp and everything works. Build both and the link step fails: two weight(int, int) bodies now occupy one region, the global namespace, and the linker reports a multiple definition of weight(int, int), pointing back at the file where it met the name earlier. Notice what played no part in that. Nothing had to call the ambiguous function; a second body carrying an existing name is enough by itself.
Renaming one of them would clear it up, at the price of hunting down every call site in a codebase you may not own. Giving each team a region of its own costs two lines and no call sites at all. The standard library made exactly this move when everything it owns went into std.
Declaring a Namespace
The namespace keyword opens a region, and a pair of braces closes it.
namespace Telemetry
{
// every name written between these braces gains a Telemetry:: prefix
}
Three rules govern where one can go and how it looks:
- A namespace may be opened at file scope or within another namespace, and nowhere else. Inside a function or a class body is not allowed.
- Its contents are indented one level, the same as the body of a function.
- The closing brace needs no semicolon after it, although the language tolerates one and you will occasionally meet code that writes it.
Older code almost always spells namespace names in lower case, and plenty of style guides still ask for that. This course capitalises them instead, for three reasons: program-defined types are already capitalised, so
Freight::weight reads consistently whether Freight turns out to be a namespace or a class; a capital initial keeps your names clear of the lower-case names that system headers hand out; and both the C++20 standard document and the C++ Core Guidelines write them that way. Either convention is defensible, so match whatever the code around you already does.
Naming a Namespace Member With ::
Give each team a region and the two totals stop competing. In a real project each namespace block would sit in its own source file, exactly as freight_lib.cpp and postal_lib.cpp did above; they are shown together here so the example runs as one program.
#include <iostream>
namespace Freight
{
int weight(int crates, int perCrate)
{
return crates * perCrate;
}
}
namespace Postal
{
int weight(int crates, int perCrate)
{
return crates * perCrate + crates;
}
}
int main()
{
std::cout << Freight::weight(7, 12) << '\n';
std::cout << Postal::weight(7, 12) << '\n';
return 0;
}
84
91
Both functions are reachable from the same line of code, with no ambiguity anywhere, because Freight::weight and Postal::weight are two different names. A name written with its region attached, like those two or like std::cout, is a qualified name; a bare weight is an unqualified name.
That distinction explains why the earlier main.cpp would still refuse to build against the namespaced libraries. Its forward declaration promises a global weight(int, int), and after the move no such entity exists, so the linker reports an undefined reference to weight(int, int). Qualifying a name changes the name, and every declaration of it has to change to match.
The scope resolution operator is one of two ways to reach a namespaced name. The other, using statements, gets a lesson of its own later in this chapter.
Reaching the Global Namespace With a Leading ::
Write :: with nothing on its left and you have asked for the global namespace specifically. That is a useful thing to be able to say when a nearer name is shadowing the one you want.
#include <iostream>
void status() // at file scope, outside every namespace
{
std::cout << "yard-wide status\n";
}
namespace Dock
{
void status()
{
std::cout << "dock 7 status\n";
}
void report()
{
status(); // Dock::status wins, no qualification needed
::status(); // lookup starts at file scope, picking a different status()
}
}
int main()
{
Dock::report();
::status();
return 0;
}
dock 7 status
yard-wide status
yard-wide status
The ::status() in main() is pure decoration: nothing is competing for the name at that point, so plain status() would have run the same function. Move the same call inside Dock and the leading :: starts doing real work, which is what the two calls in report() are for.
Unqualified Lookup Inside a Namespace
Look again at report(). Its first call names status with nothing attached, and it ran Dock::status. That follows a fixed search order, and it is worth memorising because it explains most surprises about which overload you got.
An unqualified name is hunted for in the region where it was written. Failing that, the search steps outward into whatever region encloses that one, then outward again, with the global namespace tried last of all. In report() the first stop is Dock, a status is sitting right there, and the search ends immediately. Delete Dock::status and the very same line would climb out to file scope and find the global one instead, with no edit to the call.
The second call, ::status(), refuses to play that game. Its leading :: names the global namespace outright, so the search begins and ends there no matter what Dock happens to contain.
Splitting a Namespace Across Files
Header files carry forward declarations between translation units, as covered in the header files lesson. A declaration for a namespaced entity has to sit inside a matching namespace block in the header, because the block is what makes the declared name a qualified one.
haulage.h:
#pragma once
namespace Haulage
{
int lineTotal(int crates, int perCrate);
}
main.cpp:
#include "haulage.h"
#include <iostream>
namespace Haulage
{
int lineTotal(int crates, int perCrate)
{
return crates * perCrate;
}
}
int main()
{
std::cout << Haulage::lineTotal(7, 12) << '\n';
return 0;
}
84
In a larger project the body would live in haulage.cpp rather than beside main(), and it would be wrapped in the same namespace Haulage block shown here. Getting either half wrong produces a distinct symptom, and knowing which is which saves a lot of guessing:
- Declaration outside the block, body inside it: compilation stops at the call site, with GCC reporting that
Haulagehas not been declared. The including file was never told that any such region exists. - Declaration inside the block, body outside it: compilation succeeds and the link step fails, with GCC reporting an undefined reference to
Haulage::lineTotal(int, int). The promise was made under a qualified name and kept under a global one.
Reopening a Namespace
A namespace is not a single block you have to write all at once. Name it again, in the same file or in another one, and the new block joins the region that already exists rather than starting a rival.
#include <iostream>
namespace Freight
{
constexpr int maxCrates{240};
}
namespace Postal
{
constexpr int maxParcels{60};
}
namespace Freight // reopened, so perKilo joins maxCrates
{
constexpr double perKilo{1.85};
}
int main()
{
std::cout << Freight::maxCrates << '\n';
std::cout << Freight::perKilo << '\n';
std::cout << Postal::maxParcels << '\n';
return 0;
}
240
1.85
60
Freight::maxCrates and Freight::perKilo were written in blocks separated by an unrelated namespace, and both belong to one Freight. Without this rule the standard library could not exist in the shape it has, since every one of its headers opens namespace std and contributes a slice of it; the alternative would be a single enormous header.
The same rule means nothing stops you from opening namespace std and adding your own declarations to it. Almost every such addition is undefined behaviour: std carries a special prohibition against extension by user code, with a short list of exceptions that no beginner needs.
Never add your own declarations to
namespace std.
Namespaces Inside Namespaces
A namespace may be opened inside another one, and the qualification grows a segment for each level. Unqualified lookup inside the inner region behaves exactly as described earlier: the inner region first, then the one wrapping it.
#include <iostream>
namespace Depot
{
int bayCount()
{
return 4;
}
namespace Loading
{
int slots(int perBay)
{
return bayCount() * perBay; // resolved from enclosing Depot
}
}
}
int main()
{
std::cout << Depot::Loading::slots(6) << '\n';
return 0;
}
24
slots() reaches bayCount() with no qualification because Depot is the region wrapping Loading. From main(), which is outside both, the full Depot::Loading::slots is required.
Since C++17 the two opening lines can be collapsed into one. The next program is equivalent to the previous shape, and it also shows how to add something to the outer region alone once the shorthand has been used:
#include <iostream>
namespace Depot::Loading // C++17 shorthand for a nested region
{
int slots(int bays, int perBay)
{
return bays * perBay;
}
}
namespace Depot // reopened by itself, so openGate joins Depot alone
{
void openGate()
{
std::cout << "gate open\n";
}
}
int main()
{
std::cout << Depot::Loading::slots(4, 6) << '\n';
Depot::openGate();
return 0;
}
24
gate open
Writing namespace Depot::Loading or nesting the two blocks by hand produces identical results, so pick whichever reads better in the file you are editing.
Shortening a Long Qualification With an Alias
Three-segment qualifications are tiring to type and tiring to read. A namespace alias introduces a short stand-in for a longer sequence, and unlike a namespace itself, an alias may be declared inside a function:
#include <iostream>
namespace Depot::Loading
{
int slots(int bays, int perBay)
{
return bays * perBay;
}
}
int main()
{
namespace Zone = Depot::Loading; // Zone stands for Depot::Loading
std::cout << Zone::slots(4, 6) << '\n';
return 0;
} // Zone stops existing at end of main
24
Shorter code is the small win. The larger one shows up on the day slots() moves house. Every call site says Zone::slots, so pointing the alias somewhere else is a one-line edit:
namespace Zone = Overflow::Loading;
Had those calls been written out as Depot::Loading::slots, the same move would be a find and replace across the file.
Choosing a Namespace Layout
Namespaces were built to keep names from colliding, not to express a taxonomy of your program. The evidence is in the standard library itself, which keeps thousands of names in one flat std; the newer nested regions like std::ranges appeared only because certain features arrive with enough names to start colliding inside std. Let the scale of the collision risk decide the layout:
| Situation | Layout that fits |
|---|---|
| A program only you will build and run | none needed |
| A program linking third-party libraries, some of them unnamespaced | one region around your own code |
| A library you hand to other people | one top-level region named after the library |
| Several teams contributing to one product | two segments: organisation then library, or library then module |
| A large product with reusable subsystems | three segments at most: organisation, library, module |
Module segments are a reasonable way to keep reusable code apart from the code that only this application will ever want, so physics helpers might sit under Physics:: while translation tables sit under Localization::. A directory layout can draw the same line, and often should, since it costs nothing at the call site.
Past three segments the qualifications get long enough that people start reaching for shortcuts that reintroduce the collisions you were avoiding, so treat three as a ceiling rather than a target.
Before publishing a library, ask what happens when one of its names matches a name in the program that adopts it. One region wrapped around the whole library answers that question permanently, and it hands your users a bonus: typing the library name gives their editor something to autocomplete from.
Looking Forward
Qualifying every name is explicit and unambiguous, and after a few hundred lines it also gets repetitive. The next lesson introduces using declarations and using directives, which let you drop the qualification in a controlled way, along with the traps that come with dropping it carelessly. Later in the chapter, unnamed and inline namespaces put the same syntax to two quite different purposes: sealing names inside one file, and choosing which revision of a name unqualified callers get.
Key Terminology
Namespace: a named region that owns the declarations written inside it, so that identical names in separate regions do not collide.
User-defined namespace: a namespace declared by your own program rather than by the standard library or a third-party header. Also called a program-defined namespace.
Scope resolution operator: the :: token, which directs a lookup into the region named on its left.
Qualified name: a name written with the region it belongs to attached, such as Freight::weight or std::cout.
Unqualified name: a bare name with nothing attached, resolved by searching outward from the region where it appears.
Namespace alias: a short local name introduced with namespace Short = Long::Path;, standing in for a longer sequence.
Summary
- A namespace is a named region that owns its declarations, so
Freight::weightandPostal::weightare two distinct names and never collide - Two definitions of one name in the global region are a link-time failure whether or not anything calls them, and the risk of that climbs faster than the count of names does
- A namespace is opened with the
namespacekeyword at file scope or inside another namespace; a function body or class body cannot hold one, and the closing brace needs no semicolon - The scope resolution operator searches the region on its left for the name on its right; with nothing on its left it searches the global namespace
- An unqualified name is looked up in the region containing it, then in each enclosing region in turn, with the global namespace tried last
- A forward declaration for a namespaced entity must sit inside a matching
namespaceblock, or the call site fails to compile; a definition that escapes the block compiles but fails to link - Naming a namespace again reopens it, in the same file or across many, which is how every standard library header contributes to one
std - Adding your own declarations to
namespace stdis undefined behaviour in almost every case - Namespaces nest, and C++17 lets
namespace Depot::Loadingreplace two opening lines; reopeningDepotalone still adds to the outer region only - A namespace alias shortens a long qualification and confines the change to one line when the target moves
- Publish nothing without a namespace around it, keep private programs simple, and treat three segments as the practical ceiling
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Creating Custom Namespaces - Quiz
Test your understanding of the lesson.
Practice Exercises
Creating Namespaces
Create custom namespaces to organize code and avoid naming conflicts.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!