Understanding Variable Shadowing
Recognize when inner scope variables hide outer ones and avoid related bugs.
What Is Variable Shadowing (Name Hiding)?
Variable shadowing, also called name hiding, happens when a declaration in an inner scope reuses a name that is already declared in an enclosing scope. From that point on, the name refers to the inner variable, and the outer one carries on existing with nobody able to say its name.
Nothing is destroyed, moved, or copied. Two separate objects exist at the same time, and only one of them can be reached. That gap between what the code looks like it touches and what it actually touches is where the bugs come from.
Name lookup stops at the first match
Every time the compiler meets a name, it has to decide which declaration that name refers to. It searches outward from the point of use and takes the first declaration it finds:
| Order | Where the compiler looks |
|---|---|
| 1 | the block containing the use, considering only declarations written above it |
| 2 | each enclosing block, working outward one pair of braces at a time |
| 3 | the parameter list of the enclosing function |
| 4 | enclosing namespaces, finishing with the global namespace |
The search stops at the first hit, so a match in step 1 means steps 2 to 4 are never reached. Shadowing is not a special rule; it is just what "stops at the first match" looks like when the same name exists twice.
Three blocks, three variables, one name:
#include <iostream>
int main()
{
int tickets{ 36 };
std::cout << "level 1: " << tickets << '\n';
{
int tickets{ 91 };
std::cout << "level 2: " << tickets << '\n';
{
int tickets{ 250 };
std::cout << "level 3: " << tickets << '\n';
}
std::cout << "level 2 again: " << tickets << '\n';
}
std::cout << "level 1 again: " << tickets << '\n';
return 0;
}
Output:
level 1: 36
level 2: 91
level 3: 250
level 2 again: 91
level 1 again: 36
All three variables are alive at once while the innermost block runs. Each std::cout line reaches whichever tickets is nearest, and the last two lines confirm that leaving a block hands the name back to the declaration one level out, with the value untouched.
Inside a block that shadows a local variable, the outer variable is unreachable. There is no qualifier, no keyword, and no cast that gets you back to it, because the name is the only handle you ever had.
A shadow starts where the declaration does
The inner variable does not take over the name at the opening brace. It takes over at its own declaration, so a use earlier in the same block still finds the outer variable.
#include <iostream>
int main()
{
int tickets{ 36 };
{
std::cout << "before the inner declaration: " << tickets << '\n';
int tickets{ 91 };
std::cout << "after the inner declaration: " << tickets << '\n';
}
std::cout << "back in the outer block: " << tickets << '\n';
return 0;
}
Output:
before the inner declaration: 36
after the inner declaration: 91
back in the outer block: 36
Two statements, the same spelling, two different variables. This is the shape that makes shadowing hard to read: nothing about tickets on either line tells you which object it means, and the answer depends on a declaration sitting between them.
Delete the inner declaration and the name reaches outward
The opposite mistake is just as common. A nested block that assigns to a name without declaring it first is modifying the outer variable, and the change outlives the block.
#include <iostream>
int main()
{
int tickets{ 36 };
{
tickets = 250;
std::cout << "inside the nested block: " << tickets << '\n';
}
std::cout << "back in the outer block: " << tickets << '\n';
return 0;
}
Output:
inside the nested block: 250
back in the outer block: 250
Compare this with the previous program. The two differ by one declaration, they read almost identically, and they leave the outer variable in different states. Adding or removing a single int at the top of a block silently redirects every use of that name below it.
A local can shadow a name at namespace scope
The same search order applies when the outer declaration is a global variable. A local with a matching name wins everywhere that local is in scope, and functions that declare no such local keep seeing the global.
#include <iostream>
int tempo{ 480 };
void reportTempo()
{
std::cout << "tempo seen by reportTempo: " << tempo << '\n';
}
int main()
{
int tempo{ 17 };
++tempo;
std::cout << "tempo seen by main: " << tempo << '\n';
reportTempo();
return 0;
}
Output:
tempo seen by main: 18
tempo seen by reportTempo: 480
++tempo in main increments the local. The global never changes, and reportTempo proves it by printing the original value. The shadow is confined to the scope that created it, not to the whole program.
Reaching the shadowed global with ::
Globals are the one case where the hidden variable can still be named. A global lives in the global namespace, so writing the scope resolution operator :: with nothing before it tells the compiler to skip the local search entirely and look there.
#include <iostream>
int tempo{ 480 };
int main()
{
int tempo{ 17 };
tempo += 3;
::tempo -= 30;
std::cout << "tempo inside main: " << tempo << '\n';
std::cout << "tempo at namespace scope: " << ::tempo << '\n';
return 0;
}
Output:
tempo inside main: 20
tempo at namespace scope: 450
The two statements in the middle modify two different objects, and the only visible difference between them is two colons. Being able to write ::tempo makes the situation recoverable; it does not make it readable.
Parameters and loop variables cast shadows too
Shadowing is not limited to variables you declare with an explicit nested block. A function parameter is declared in the function's own scope, so a parameter that reuses a global's name shadows it for the entire body. A for loop variable is declared in a scope that wraps the loop, so it shadows anything of the same name outside.
#include <iostream>
int tempo{ 480 };
void announceTempo(int tempo)
{
std::cout << "parameter tempo: " << tempo << '\n';
std::cout << "namespace scope tempo: " << ::tempo << '\n';
}
int main()
{
announceTempo(96);
int step{ 2 };
for (int step{ 1 }; step <= 3; ++step)
{
std::cout << "loop step: " << step << '\n';
}
std::cout << "outer step: " << step << '\n';
return 0;
}
Output:
parameter tempo: 96
namespace scope tempo: 480
loop step: 1
loop step: 2
loop step: 3
outer step: 2
Neither of these needed a stray pair of braces to go wrong. Parameter names are chosen for what the function does, loop variables are usually short, and both are easy to collide with something declared further out.
A shadowed name produces no error and, with the platform's flags, no warning. The program compiles, runs, and quietly reads or writes the variable you did not mean. Reviewing the code will not help either, since the two spellings are identical.
Getting the compiler to report it
GCC and Clang can flag shadowing on request through -Wshadow, which is not part of -Wall or -Wextra and is therefore off in this platform's build (-std=c++20 -Wall -Wextra -O2). Compiling the first program in this lesson with -Wshadow added produces warning: declaration of 'tickets' shadows a previous local [-Wshadow] for each nested declaration, with a follow-up note pointing at the declaration being shadowed. The parameter in the last program reports warning: declaration of 'tempo' shadows a global declaration [-Wshadow].
If those warnings are too noisy for an existing codebase, GCC accepts narrower forms such as -Wshadow=local and -Wshadow=compatible-local, which report only the local-on-local cases.
The warning is a safety net rather than a solution. The real fix is to never create the collision:
Do not reuse a name that is already visible at the point you are declaring. Give the inner variable a name that says how it differs, and prefix global variables with
g_ so a local can never collide with one by accident.
Renaming costs nothing and removes the ambiguity permanently. A g_tempo global cannot be shadowed by a local called tempo, and a reader who sees g_ knows immediately which one is on screen.
Summary
Name lookup searches outward from the point of use and stops at the first declaration it finds. Shadowing is the consequence: an inner declaration that reuses an outer name takes ownership of that name for the rest of its scope.
The outer variable is not affected in any way. It keeps its value, keeps its storage, and becomes reachable again as soon as the inner scope ends. What it loses is its name, and for a shadowed local that loss is total, because a local has no qualified form.
Globals are the exception. A global shadowed by a local is still reachable as ::name, since the global namespace can be named explicitly even when the plain name resolves somewhere closer.
Shadowing arrives through nested blocks, function parameters, and loop variables alike, and none of those forms is diagnosed by default. Enable -Wshadow if you want the compiler watching for it, prefix globals with g_, and pick distinct names in nested scopes so the question of which variable a line touches never comes up.
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.
Understanding Variable Shadowing - Quiz
Test your understanding of the lesson.
Practice Exercises
Variable Shadowing
Understand what happens when a local variable has the same name as an outer variable.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!