C++11
An inline namespace is a namespace whose members are also members of the enclosing namespace — callers can name them with or without the inner namespace. That sounds like a small convenience; it is actually the language's mechanism for versioning an API under a stable name, and it is load-bearing inside every major standard library implementation.
The core mechanic
#include <print>
#include <string_view>
namespace geo {
namespace v1 {
constexpr double area(double r) { return 3.14 * r * r; } // the old, sloppy pi
constexpr std::string_view version() { return "v1"; }
}
inline namespace v2 {
constexpr double area(double r) { return 3.14159265358979 * r * r; }
constexpr std::string_view version() { return "v2"; }
}
}
int main() {
// Unqualified use gets the inline namespace - the "current" version:
std::println("default ({}): {:.6f}", geo::version(), geo::area(1.0));
// Callers who need the old behavior pin themselves explicitly:
std::println("pinned ({}): {:.6f}", geo::v1::version(), geo::v1::area(1.0));
}
geo::area and geo::v2::area are the same function — not a copy, not a forwarder. When v3 ships, the library moves the inline keyword; unpinned callers get v3 on their next recompile, and pinned callers keep exactly what they asked for. The version selection can even be a build flag:
namespace net {
#if defined(NET_PREVIEW)
inline namespace v3 { /* next generation */ }
namespace v2 { /* stable */ }
#else
namespace v3 { /* opt-in preview */ }
inline namespace v2 { /* stable is the default */ }
#endif
}
Why not just a using-declaration?
namespace geo { using v2::area; } gets you the name, but inline namespaces preserve the deeper semantics that versioning actually needs:
- Argument-dependent lookup. For a type defined in
geo::v2, ADL finds operators and free functions whether callers think ingeoorgeo::v2. - Template specialization. Users can specialize
geo::hash<T>from outside even though it really lives ingeo::v2— impossible across a plainusing. - Distinct mangled names — the ABI point. The linker symbol for
geo::v2::areacontains thev2. Compile one.cppagainst the v2-inline headers and another against v3-inline headers, and they cannot accidentally resolve to each other's symbols. Version skew becomes a loud link error instead of a silent crash at 2 a.m. This is the entire reason libstdc++ shipped its C++11-conformingstd::stringinsidestd::__cxx11— old and new string layouts coexist in one process without ever cross-linking.
Where you already use them
The standard library's literal suffixes all live in inline namespaces so that one using brings in exactly the set you want:
#include <chrono>
#include <print>
#include <string>
int main() {
using namespace std::literals; // string_literals + chrono_literals + ...
auto name = "modern"s; // std::string, not const char*
auto wait = 150ms; // std::chrono::milliseconds
std::println("{} for {}", name, wait);
}
std::literals, std::literals::string_literals, and std::literals::chrono_literals are nested inline namespaces — which is why using namespace std::literals;, using namespace std::string_literals;, and even plain std::chrono usage all compose without conflict.
Since C++20, the shorthand namespace geo::inline v2 { } declares the nested inline namespace in one line.
Practical rules
- The
inlinekeyword must be consistent: a namespace is inline from its first declaration, in every declaration. - Exactly one version namespace should be inline at a time per build — two inline siblings with the same function names makes unqualified calls ambiguous.
- Reopening the namespace elsewhere may omit the keyword, but writing it everywhere is kinder to readers.
- Inline namespaces are for publishers of libraries. Application code that isn't maintaining API/ABI compatibility across versions rarely needs one — reach for it when you have downstream users you can't recompile.
Guidelines
- Version a library as
lib::vNnamespaces withinlinemarking the current default; document that pinning tolib::v1::is the compatibility escape hatch. - Flip which namespace is inline in exactly one place (ideally via one macro/config header) so a build can never see two defaults.
- Treat the mangled-name distinctness as a feature: after an ABI-relevant change, bump the namespace so stale objects fail to link instead of misbehaving.
- Don't nest unrelated machinery in inline namespaces "for organization" — every level is another name for the same things, and that multiplies confusion, not clarity.