Scope
How C++ scope rules govern name visibility, lookup order, and entity lifetime — from block and function scope to namespace and class scope.
Scopesince C++98The scope of a name is the contiguous region of program text in which that name can be used without qualification.
Overview
Scope is one of the most pervasive concepts in C++. Every name — variable, function, type, template, namespace — has a scope that determines where it is visible. Understanding scope is prerequisite knowledge for name lookup, linkage reasoning, lifetime analysis, and API design.
C++ defines several distinct scope categories:
| Scope kind | Introduced by |
|---|---|
| Block scope | A {...} compound statement |
| Function parameter scope | A function declarator's parameter list |
| Function scope | The entire function body (labels only) |
| Namespace scope | A namespace definition or the implicit global namespace |
| Class scope | A class, struct, or union definition |
| Enumeration scope | An enum class / enum struct (C++11) |
| Template parameter scope | A template parameter list |
Block Scope
A name declared inside braces has block scope: it is visible from its point of declaration to the closing brace of the innermost enclosing block.
void process(int n) {
if (n > 0) {
int result = n * 2; // visible until '}'
use(result);
}
// result is not in scope here — compile error if referenced
}A name in an inner block may shadow a name in an enclosing scope. The outer name becomes inaccessible by simple name; a qualified name is required:
int x = 10;
void f() {
int x = 20; // shadows ::x
::x = 30; // explicit qualification reaches the global x
}Shadowing is legal but a persistent source of subtle bugs. Enable -Wshadow (GCC/Clang) or /W4 (MSVC) to catch unintentional hiding.
for-loop scope
A variable declared in a for init-statement is scoped to the entire loop — condition, increment, and body — but not beyond:
for (int i = 0; i < n; ++i) {
// i is in scope here
}
// i is not in scope here — intentional containmentif / switch initialisers (C++17)
Since C++17, an optional init-statement may appear between the keyword and the condition in if and switch. The declared variable is in scope for the entire if-else chain but not for the surrounding block:
// C++17
if (auto it = map.find(key); it != map.end()) {
use(it->second);
}
// it is out of scope — iterator not leaked into surrounding code
// Useful for scoping a lock tightly:
if (std::lock_guard<std::mutex> lg{mu}; !queue_.empty()) {
process(queue_.front());
queue_.pop();
} // lg released here, not at end of enclosing functionNamespace Scope
A name declared directly inside a named namespace, or at global scope (the implicit global namespace ::) has namespace scope. Such names persist for the entire translation unit and are reachable from other translation units when they carry external linkage.
namespace geometry {
struct Point { double x, y; }; // namespace scope
double distance(Point a, Point b); // namespace scope
}
geometry::Point origin{0.0, 0.0}; // qualified access from outsideThe unary ::name form explicitly names the global namespace, useful for disambiguating when a local name shadows a global one.
Class Scope
Names declared inside a class body have class scope and are accessible via the class name (for static members and nested types), via an object or reference (for non-static members), or unqualified within member function bodies.
class Timer {
public:
void start();
void stop();
private:
using Clock = std::chrono::steady_clock; // class scope
Clock::time_point begin_;
};
void Timer::start() {
begin_ = Clock::now(); // Clock is visible in class scope
}A critical property: the entire class body is a single scope, so all member declarations are mutually visible regardless of order. This differs from block scope, where a name is only visible after its point of declaration.
struct Foo {
void f() { g(); } // OK — g declared below, still visible
void g() {}
};This property enables forward references among member functions and is what makes circular member references (e.g., next_ pointing to Foo*) work without forward declarations inside the class.
Enumeration Scope (C++11)
Unscoped enumerations inject their enumerators into the enclosing scope, polluting it with bare names. Since C++11, scoped enumerations (enum class or enum struct) confine enumerators to the enumeration's own scope, requiring explicit qualification:
enum Color { Red, Green, Blue }; // Red, Green, Blue pollute enclosing scope
enum class Direction { North, South, East, West }; // C++11
Direction d = Direction::North; // bare 'North' does not exist herePrefer enum class in all new code, especially in headers included widely, to avoid silent name collisions.
Scope vs. Lifetime
Scope is a compile-time, textual property: it determines where a name is visible in source. Lifetime (storage duration) is a runtime property: it determines when the underlying object exists in memory. The two are related but independent.
void counter() {
static int n = 0; // block scope — 'n' visible only here
++n; // but the object lives for the program's lifetime
}
int* p = new int{42}; // the object outlives any named variableA local static is the canonical example of narrow scope combined with extended lifetime. A dynamically allocated object is the dual: no named scope at all, but lifetime governed entirely by delete (or a smart pointer destructor).
Scope and Name Lookup
When the compiler resolves an unqualified name, it searches scopes outward from the point of use: innermost block, enclosing blocks, enclosing namespace(s), the global namespace. Argument-dependent lookup (ADL) additionally searches the namespaces associated with a call expression's argument types — the mechanism that makes operator overloads and customisation points work across namespace boundaries.
namespace math {
struct Vec2 { float x, y; };
Vec2 operator+(Vec2 a, Vec2 b) { return {a.x + b.x, a.y + b.y}; }
}
void f() {
math::Vec2 u{1, 0}, v{0, 1};
auto w = u + v; // ADL finds math::operator+ without any qualification
}Best Practices
Declare names in the narrowest scope that satisfies correctness. This shrinks the window in which a variable can be misused, makes local reasoning possible, and gives destructors a predictable invocation point.
// Prefer — key scoped to the loop iteration
for (const auto& rec : records) {
std::string key = compute_key(rec);
cache[key] = rec;
}
// Avoid — key outlives the loop for no reason
std::string key;
for (const auto& rec : records) {
key = compute_key(rec);
cache[key] = rec;
}Use C++17 if-init to bind lookup results and guards at the point of use, preventing them from leaking into code that should not see them.
Prefer enum class over plain enum to keep enumerator names out of the enclosing namespace.
Common Pitfalls
Dangling references to block-scoped objects. Returning a pointer or reference to a local variable is undefined behaviour; the object ceases to exist at the closing brace.
int* bad() {
int x = 42;
return &x; // UB: x is destroyed on return
}Lambda captures and scope. A lambda that captures a local by reference must not outlive that variable's scope. This is a common error when lambdas are stored in callbacks or dispatched to thread pools:
std::function<int()> make_counter() {
int n = 0;
return [&n]() { return ++n; }; // UB: n is destroyed on return
// Fix:
return [n]() mutable { return ++n; }; // capture by value
}Unscoped enum leakage. Adding an enumerator to a widely included plain enum can silently clash with other names in the enclosing namespace, introducing a breaking change for all consumers of that header.
Variable shadowing across if-else branches. When an if-init variable is shadowed inside the body, the outer variable — including any RAII guard — may be partially invisible, obscuring which name is active.
See Also
reference/idioms/scope-guard— RAII wrapper that executes a callback when a scope is exited, regardless of howreference/language/adl— how argument-dependent lookup extends unqualified name search beyond the immediately enclosing scopes