Chapter 13 of 24 | C++
Scope, lifetime, and visibility
Scope answers where a name can be used; lifetime answers how long an object exists. These concepts interact, but they are not the same. A local name may go out of scope while a reference or pointer to its object is still stored elsewhere, leaving that access invalid.
Common scopes
- Block scope: a local variable declared inside braces is visible from its declaration to the end of that block.
- Namespace scope: declarations outside functions belong to a namespace or the global namespace.
- Class scope: members are accessed according to the class interface and access rules.
- Function-parameter scope: parameters are available within the function body.
Prefer local variables and narrow scopes. A variable that exists only where needed is easier to reason about and less likely to be accidentally changed.
Storage duration and lifetime
int global_count{}; // static storage duration
void visit()
{
int visits{}; // automatic storage duration
static int total_visits{}; // static storage duration
++visits;
++total_visits;
}
An automatic local object normally begins its lifetime when execution reaches its declaration and ends when its block exits. A static-storage object exists for the duration of the program. A static local is initialized the first time control reaches its declaration and then persists between calls.
Dynamic objects have lifetimes controlled by their owning resource-management object. Prefer ownership types instead of trying to track dynamic lifetime manually. Never return a pointer or reference to an automatic local object.
Namespaces and name lookup
Namespaces group names and help avoid collisions. The standard library places its names in std, which is why code writes std::string. Avoid using namespace std; in headers: it can introduce unexpected names into every file that includes the header. A local using-declaration can be reasonable in a small implementation file.
Use constexpr for values that can be computed at compile time. Use constinit when a static or thread-local variable must have constant initialization. Avoid complicated global initialization dependencies.
Lifetime boundaries and temporaries
Objects are created and destroyed according to their storage duration and the rules of their type. A local object with automatic storage duration is destroyed when its block exits, including when a return statement leaves the function. This deterministic destruction is the foundation of RAII. If an object owns other objects as members, those members are destroyed automatically as part of the containing object's destruction.
Temporary objects often live until the end of the full expression in which they are created. Binding a temporary to a local const reference can extend its lifetime to that reference's scope, but storing a reference returned from a function or inside a data structure has different rules. Do not infer safety merely because an expression appears to name a temporary; identify who owns the object and exactly how long it lives.
Shadowing and declarations
A declaration in an inner scope can shadow a name from an outer scope. Although legal in many cases, shadowing can make it hard to tell which variable a statement refers to. Use distinct names or smaller scopes, and enable compiler warnings that point out suspicious shadowing.
A declaration introduces a name into a scope. A name generally cannot be used before it is declared, and access can also be limited by class visibility. Keep declarations close to their first use. This narrows the region where the name is available and makes its purpose easier to discover.
Global state and initialization order
Global mutable objects can be changed by many parts of a program, which makes the source of a bug hard to find. Prefer passing the required value to a function or storing it in an object that owns the relevant behavior. A namespace-scope constant can be appropriate for a true immutable configuration value, but its initialization should not depend unpredictably on another translation unit.
Static initialization happens before ordinary program execution, but dynamic initialization of global objects across translation units can have ordering hazards. Local static objects avoid some ordering problems because initialization happens the first time control reaches their declaration. They still represent program-wide state and should be used for a clear reason rather than as an automatic substitute for parameters.
Linkage controls identity across files
Linkage determines whether declarations in different translation units can refer to the same entity. External linkage is commonly used for a function implemented in one source file and declared in a header. Internal linkage restricts an entity to one translation unit. Namespaces help organize declarations but do not by themselves remove the need to follow the One Definition Rule.
In modern C++, prefer unnamed namespaces in source files for implementation-only names instead of applying static to a free function or object. Keep headers free of ordinary non-inline definitions that would create a separate definition in every source file that includes them.
Practice
- Identify the scope and lifetime of every variable in a short function.
- Find an example of a dangling reference and explain its lifetime mismatch.
- Move a global variable into the smallest scope that needs it.
- Find a shadowed name and rename or restructure the declarations to remove ambiguity.
- Track the construction and destruction order of a local object and its members.
- Explain which declarations in a two-file program need external linkage.
Key idea: visibility controls where a name can be used; lifetime controls how long its object is valid. Design both deliberately.
Static initialization and the one-definition rule
Namespace-scope objects with static storage duration are initialized in stages. Constant initialization can happen before dynamic initialization; dynamic initialization across separate translation units has ordering constraints that are easy to misuse. If one global object's constructor depends on another global object in a different source file, the order may not be what the programmer expects. Prefer function-local statics, explicit initialization, or dependency injection when initialization order matters.
The One Definition Rule applies across translation units as well as within a file. Some entities may have multiple definitions only under specific conditions, such as equivalent inline definitions, but ordinary non-inline function and variable definitions must have a single program-wide definition. Violations can produce linker errors or, in some cases, ill-formed programs for which no diagnostic is required.
Linkage, storage duration, and visibility are separate
Scope describes the region where a name can be found; linkage describes whether declarations can denote the same entity across scopes or translation units; storage duration describes how long storage exists. Access control is yet another rule governing whether code may use a class member. A name can be in namespace scope but have internal linkage, or have static storage duration while being visible only inside a function.
Keeping these terms distinct prevents common confusion around the keyword static, which has different effects depending on context. At namespace scope it can give internal linkage to a variable or function. For a local variable it gives static storage duration. For a class member it denotes one member shared by the class rather than a member per object.
Thread storage and thread safety
thread_local gives an object a separate instance for each thread. Its lifetime and initialization are associated with that thread, not simply the whole process. Thread-local state can reduce synchronization for per-thread data, but it can complicate cleanup, testing, and assumptions about worker-thread reuse.
Function-local static initialization is synchronized so concurrent first calls initialize the object once in modern C++. That does not make later operations on the object thread-safe. Once initialization completes, concurrent mutation still needs synchronization.
Advanced exercise
- Classify several declarations by scope, linkage, and storage duration independently.
- Construct a two-file example with unsafe global initialization dependencies, then refactor it to explicit initialization.
- Explain why one-definition-rule violations may not always produce a clear linker diagnostic.
- Use a function-local static in a multithreaded test and distinguish safe initialization from safe mutation.