Variable Scope and Lifetime Management Summary
Review and test your understanding of all variable scope and lifetime management concepts covered in this chapter.
What Is a Declaration Actually Deciding?
Every declaration you write settles three separate questions, whether or not you thought about them at the time:
- Where in the source can this name be written? That is its scope.
- When does the thing behind the name get built and torn down? That is its duration.
- Which translation units are allowed to bind to the same entity? That is its linkage.
The three are independent. A name can be visible in one block but survive the whole run. A name can be visible for the rest of a file yet be unreachable from any other file. Every keyword this chapter introduced is a lever on one of those three answers.
This recap is arranged the way you actually meet the material: start from what you are trying to accomplish, find the tool, then read the section that explains the machinery underneath.
Start From the Goal
| What you want | What you write | Which lever it pulls |
|---|---|---|
| Run several statements where the grammar allows only one | Wrap them in a block: { ... } |
Scope |
| Stop your names from colliding with a library's names | Put yours in a user-defined namespace, reach in with :: |
Scope |
| Carry a value from one call of a function to the next | A static local |
Duration |
Give every function in one .cpp file access to one object |
A global inside an unnamed namespace | Linkage |
Let code in other .cpp files reach one object |
A global plus an extern forward declaration in the other file |
Linkage |
| Publish a constant from a header that many files include | inline constexpr inside a namespace in the header |
Linkage |
Ship a whole utility with no .cpp file at all |
Mark its functions inline |
Linkage |
| Shorten one long namespace path without importing anything | A namespace alias | Scope |
| Add a new version of an API without editing call sites | An inline namespace | Scope |
Question One: Where Can the Name Be Written?
Wrap any run of statements in braces and the result counts as a single statement everywhere the grammar asks for one. That construct is a compound statement, though almost everyone calls it a block. A block with zero statements inside is legal, and no trailing semicolon is needed. This is why an if or a loop body can carry as much work as you like: the grammar only ever wanted one statement, and a block is one.
Local variables, which includes function parameters, have block scope. The name becomes usable at its point of definition and stops being usable at the closing brace of the enclosing block. Nested blocks can see outward, but a name declared in a nested block can shadow (or name hide) an identically named one further out, and while the compiler is content, a reader rarely is.
A user-defined namespace is one you declare yourself for your own identifiers. The global namespace and std do not qualify, since neither came from your code. You reach into a namespace of any origin with the scope resolution operator ::, which takes a scope on its left and confines the search for the name on its right to that scope. Leave the left side blank and the search happens in the global namespace.
A name written with a scope attached, such as std::string or Garage::levelCount, is a qualified name. The bare form, string or levelCount, is an unqualified name.
This program deliberately shadows a variable so you can watch lookup pick the nearest declaration:
#include <iostream>
namespace Garage
{
constexpr int levelCount{4};
}
int main()
{
int freeBays{18};
std::cout << "outer block sees freeBays = " << freeBays << '\n';
{
int freeBays{2}; // a second, unrelated variable that hides the first
std::cout << "inner block sees freeBays = " << freeBays << '\n';
}
std::cout << "outer block still sees freeBays = " << freeBays << '\n';
std::cout << "Garage::levelCount = " << Garage::levelCount << '\n';
return 0;
}
outer block sees freeBays = 18
inner block sees freeBays = 2
outer block still sees freeBays = 18
Garage::levelCount = 4
The inner freeBays is a different object with its own storage. Assigning to it inside that block would leave the outer one at 18. Shadowing is legal and occasionally unavoidable, but it costs the reader a moment of doubt every time, so prefer a distinct name.
Shortening the Names You Type
Two constructs let you drop the qualifier. A using declaration picks out a single entity, after which you may spell it bare for the rest of the enclosing scope:
#include <iostream>
#include <string>
int main()
{
using std::string; // using declaration: one unqualified alias, nothing else imported
string label{"Level 3, bay 12"};
std::cout << "printed through an unqualified name: " << label << '\n';
return 0;
}
printed through an unqualified name: Level 3, bay 12
A using directive, written using namespace std;, is the blunt instrument: it drags every identifier from the named namespace into the scope containing the directive. That is thousands of names you did not choose and cannot see, any one of which can turn a call you meant into a call you did not.
Both forms are worst inside a header, because the effect follows every file that includes it, and the file that eventually breaks will be one whose author never opted in. Keep writing `std::` and let the qualifier do the documenting.
Nesting Namespaces, and Renaming the Path
Namespaces nest, and a nested one is written in full with :: at every level. When the nesting gets deep enough that the qualifier is doing more typing than documenting, a namespace alias gives the whole path a shorter spelling. An alias is a declaration like any other, so it obeys the scope it sits in, and unlike a using directive it imports nothing: the short name and the long name are two ways to write one path.
#include <iostream>
namespace Garage::Billing
{
constexpr int lateFeeCentsPerHour{40};
int lateFee(int hoursOverdue)
{
return hoursOverdue * lateFeeCentsPerHour;
}
}
int main()
{
namespace Fees = Garage::Billing; // an alias imports nothing, it just renames a path
std::cout << "three hours overdue costs " << Fees::lateFee(3) << " cents" << '\n';
std::cout << "the long spelling agrees: " << Garage::Billing::lateFee(3) << '\n';
return 0;
}
three hours overdue costs 120 cents
the long spelling agrees: 120
Fees stops being a name at the end of main(), which is the point: the shortening is local to the code that wanted it, and no other function inherits it.
Question Two: How Long Does the Storage Live?
Local variables have automatic storage duration: the object is created when control reaches the definition and destroyed when control leaves the block, every single time. Globals have static duration: one object, built before main() begins and torn down after main() returns.
The static keyword on a local variable keeps block scope but swaps automatic duration for static duration. The name is still spellable only inside that function, yet the object outlives every call:
#include <iostream>
int nextTicket()
{
static int counter{5000}; // static duration: one object, created before main() runs
int stamped{counter}; // automatic duration: rebuilt and destroyed every call
++counter;
return stamped;
}
int main()
{
std::cout << "first car takes ticket " << nextTicket() << '\n';
std::cout << "second car takes ticket " << nextTicket() << '\n';
std::cout << "third car takes ticket " << nextTicket() << '\n';
return 0;
}
first car takes ticket 5000
second car takes ticket 5001
third car takes ticket 5002
stamped starts over on every call because its storage does. counter does not, because there is only ever one of it.
Prefer initializing static-duration variables with constant expressions. Initializing one by calling a function, reading a stream, or copying another global drags in ordering questions that the language answers only within a single translation unit, and across translation units does not answer at all.
Question Three: Which Translation Units Can Bind to It?
Linkage is the rule that decides when two identical spellings, written in different places, are two views of one entity rather than two entities that merely look alike.
- No linkage: every declaration is its own entity. Local variables, including static locals, work this way. Two functions can each hold a
static int counterwithout either knowing about the other. - Internal linkage: usable anywhere in the translation unit that declares it, invisible to every other one. You get it from
staticon a global, fromconstandconstexprglobals by default, and from anything inside an unnamed namespace. - External linkage: one entity that any translation unit can bind to, provided it forward-declares the name. Functions and non-const globals get this by default.
Here is a two-file program that uses all three. First a header, and note that this header is a complete miniature library. It has no companion .cpp file, which is what inline makes possible:
#pragma once
namespace Garage
{
inline constexpr int bayCount{60};
inline int baysFree(int occupied)
{
return bayCount - occupied;
}
}
Now the implementation file, holding one identifier of each linkage flavor:
#include "garage_limits.h"
#include <iostream>
namespace // unnamed namespace: everything inside gets internal linkage
{
int barrierLifts{0};
}
int occupiedBays{57}; // external linkage, and exactly the kind of global to avoid
void openBarrier()
{
++barrierLifts;
++occupiedBays;
std::cout << "barrier lift #" << barrierLifts
<< ", bays still free: " << Garage::baysFree(occupiedBays) << '\n';
}
And the file with main(), which reaches the external names by declaring them without defining them:
#include "garage_limits.h"
#include <iostream>
extern int occupiedBays; // forward declaration: gate.cpp owns the definition
void openBarrier();
int main()
{
std::cout << "capacity from the header: " << Garage::bayCount << '\n';
openBarrier();
openBarrier();
std::cout << "main can read the same object: occupiedBays = " << occupiedBays << '\n';
return 0;
}
capacity from the header: 60
barrier lift #1, bays still free: 2
barrier lift #2, bays still free: 1
main can read the same object: occupiedBays = 59
barrierLifts sits in an unnamed namespace, so it has internal linkage and no extern declaration anywhere else could ever reach it. occupiedBays has external linkage, and the extern line in the second file is a forward declaration rather than a definition, which is why the two files share one object rather than fighting over two.
occupiedBays is also the mistake in the program. A mutable global that two files can write is the pattern this chapter spent a lesson warning about: any function anywhere can change it, so a wrong value tells you nothing about who wrote it. Constants are the exception worth making, and Garage::bayCount shows the shape they should take.
Avoid non-const global variables. Const and `constexpr` globals are fine, and when several files need the same one, put an `inline constexpr` variable in a header rather than an `extern` declaration in each file.
What inline Means Now
inline began life as a request for inline expansion, the optimization that replaces a call with a copy of the called function's body. Compilers stopped taking the hint seriously a long time ago and now decide expansion for themselves. The keyword survives with a different job: it grants permission for multiple definitions. An inline function or variable may be defined in every translation unit that uses it without breaking the one-definition rule, and C++17 extended the same permission to variables.
That permission comes with two obligations:
- Every translation unit that uses the entity must see its full definition. A forward declaration alone will not do, though the definition may come after the point of use if a forward declaration precedes it.
- Every definition must be identical. Differing definitions are undefined behavior, and typically a silent one, since the linker keeps whichever copy it happens to see first.
Both obligations are met automatically when the definition lives in a header and every user includes that header. That is exactly the arrangement a header-only library relies on, and it is why garage_limits.h above needs no .cpp file.
Reading a Declaration You Did Not Write
The three answers are readable straight off the page. Work down this table when a declaration in unfamiliar code is behaving in a way you did not expect:
| Declaration | Spellable where | Object lives | Reachable from other files |
|---|---|---|---|
int retries{}; inside a function |
To the end of that block | Until the block exits | No |
static int retries{}; inside a function |
To the end of that block | Whole program run | No |
int g_retries{}; at file scope |
To the end of that file | Whole program run | Yes, via extern int g_retries; |
static int g_retries{}; at file scope |
To the end of that file | Whole program run | No |
constexpr int maxRetries{4}; at file scope |
To the end of that file | Whole program run | No, each file gets its own |
inline constexpr int maxRetries{4}; in a header |
Every file that includes it | Whole program run | Yes, all files share one object |
The last two rows are the pair worth memorizing. A plain constexpr global in a header gives each including file its own private copy, which is harmless for reading a number and wrong the moment you take its address. Adding inline collapses them into one entity.
Versioning a Namespace Without Touching Call Sites
An inline namespace is a nested namespace whose contents also count as members of its parent. Callers that name the parent get the inline one; callers who want an older revision still name it explicitly:
#include <iostream>
namespace Garage
{
namespace v1
{
int chargeCents(int minutes) { return minutes * 5; }
}
inline namespace v2
{
int chargeCents(int minutes) { return minutes * 7; }
}
}
int main()
{
std::cout << "asking for v1 explicitly: " << Garage::v1::chargeCents(30) << '\n';
std::cout << "asking for v2 explicitly: " << Garage::v2::chargeCents(30) << '\n';
std::cout << "asking for neither: " << Garage::chargeCents(30) << '\n';
return 0;
}
asking for v1 explicitly: 150
asking for v2 explicitly: 210
asking for neither: 210
Moving inline from v2 to v1 would flip every unversioned call site back to the old pricing without any of them being edited. It is primitive versioning, but it costs nothing.
Habits Worth Keeping
- Declare each variable in the narrowest block that can hold it. Short scope means fewer places a value can go wrong.
- Do not reuse a name that is already in scope. Shadowing compiles and then misleads.
- Reach for a namespace before you reach for a longer prefix on every name.
- Keep globals const. If one must be mutable, keep it internal to a single file and let functions be the only way in.
- Write
std::rather than importing the namespace, especially in headers.
Key Terminology
- Compound statement (block): Braces around any run of statements, including none, that the grammar counts as one statement
- User-defined namespace: A namespace you declare yourself, as opposed to
stdor the global namespace - Scope resolution operator (
::): Directs lookup of the right-hand identifier into the scope named on the left - Namespace alias: A second, usually shorter, name for an existing namespace path, valid only in the scope that declares it
- Block scope: Usable from the point of definition to the closing brace of the enclosing block
- File scope: Usable from the point of declaration to the end of the file
- Automatic storage duration: Created at the definition, destroyed when the block exits
- Static duration: Created before
main()runs, destroyed after it returns - Shadowing (name hiding): An inner declaration making an identically named outer one unreachable
- Linkage: The rule deciding when identical spellings in different places are views of one entity
- No linkage: Every declaration of the name is a separate entity
- Internal linkage: One entity, reachable only within the declaring translation unit
- External linkage: One entity, reachable from any translation unit that forward-declares it
- Qualified name: A name written with its scope attached, such as
std::string - Unqualified name: A name written without a scope qualifier, such as
string - Using declaration: Grants one specific entity the right to be spelled bare inside one scope
- Using directive: Imports every identifier from a namespace into the surrounding scope
- Inline expansion: Replacing a call with a copy of the called function's body
- Inline function or variable: One that may be defined identically in multiple translation units
- Header-only library: A library delivered entirely in headers, with no
.cppfile - Unnamed namespace: A namespace with no name, whose contents get internal linkage
- Inline namespace: A nested namespace whose members also belong to the enclosing namespace
- Storage class specifier: A keyword such as
staticorexternthat sets duration and linkage
Looking Forward
Scope, duration, and linkage are the rules that decide which parts of a program can see each other, and they stop being background detail the moment a program outgrows one file. The next chapter introduces classes, which add a fourth kind of scope: names that live inside a type. Member variables, access specifiers, and constructors all build directly on the vocabulary here, and the reasoning you have been practicing, asking who can see this name and how long this object lasts, is exactly the reasoning that makes encapsulation worth having.
Summary
Three independent questions: Scope decides where a name can be written, duration decides when the object is built and destroyed, and linkage decides which translation units bind to the same entity. Every keyword in this chapter adjusts one of them.
Blocks and namespaces: A block is braces around any run of statements, and it stands in wherever the grammar wants one statement. A user-defined namespace keeps your identifiers away from other people's, and :: reaches into any scope by name. Namespaces nest, and a namespace alias shortens a long path in one scope without importing anything. A qualified name carries its scope, an unqualified name does not.
Duration: Local variables get automatic duration and are rebuilt every time control passes their definition. Globals and static locals get static duration and exist for the whole run. Prefer constant initializers for anything with static duration.
Linkage: Local variables have no linkage. static on a global, an unnamed namespace, and plain const or constexpr at file scope all give internal linkage. Functions and non-const globals default to external linkage, which other files reach through an extern forward declaration.
Inline: In modern C++ inline means multiple identical definitions are permitted, not that expansion is guaranteed. Every user must see the full definition and every definition must match, which is what makes header-only libraries and shared inline constexpr constants work.
Habits: Avoid shadowing, avoid non-const globals, avoid using directives, and keep every declaration in the narrowest scope that still does the job.
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.
Variable Scope and Lifetime Management Summary - Quiz
Test your understanding of the lesson.
Practice Exercises
Scope and Lifetime Demonstration
Create a program that demonstrates your understanding of variable scope, duration, and linkage. The program uses local variables, static local variables, and namespaces to track function calls and maintain state.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!