Skip to content
VLSI Mentor

C++ Part I · Module 7 · Scope, Storage, and Namespaces

Scope: Where a Name Is Visible

This module exists because three different questions get answered with the same word, and confusing them is what makes several later topics feel arbitrary. Where is a name visible? How long does an object exist? Can another translation unit see it? Those are genuinely independent — an object can be alive in a place where its name means nothing, and a name can be visible where the object it refers to has already ceased to exist — and this chapter takes only the first of them. Scope is about names, not objects, and it answers exactly one question: given a name written at a particular point in a program, which declaration does it refer to? The answer is a single rule applied to a nest of regions, which makes it one of the few topics in C++ where knowing the rule is genuinely sufficient. The chapter spends its length on the places the rule produces an answer people do not expect: a declaration that quietly hides another one with no warning at the flags this curriculum has used throughout, and a statement whose own variable is already in scope inside its own initializer, which the compiler will tell you about if you look.

Foundation16 min readC++Part IFundamentalsScope

Part I · Module 7 · Chapter 7.1 · Scope: Where a Name Is Visible

1. Three Questions, and This Chapter Takes One

Beginners — and plenty of non-beginners — use one word for three things:

QuestionChapter
Where is this name visible?7.1, this chapter
How long does this object exist?7.2, 7.3
Can another translation unit see this name?7.5

They are independent. Chapter 7.2 will show an object that is alive in a place where its name is meaningless, and Chapter 6.3 §6 already showed a name that was perfectly visible while the object it referred to was about to stop existing.

This chapter is only the first question, and it is about names. Not storage, not lifetime, not linkage. If you find yourself asking "but when is it destroyed?" the answer is Chapter 7.2 and the question is a different one.

One debt stays unpaid here. Every example in Module 6 was marked static, and Chapter 6.1 §3 asked you to read it as "not visible outside this file" and move on. That is the third question, and Chapter 7.5 is where it is answered properly.

2. Scope Is a Region of the Program Text

The scope of a name is the region of source text in which that name can be written and will refer to a particular declaration.

Two consequences of that wording, both worth noticing now.

Scope is a property of a name, not of an object. The same object can have several names with different scopes — Chapter 6.4 established that a reference parameter is another name for the caller's object, and those two names have completely different scopes.

Scope is a region of text, decided at compile time. It has nothing to do with when the program runs. A name is either in scope at a point in the source or it is not, and the answer does not depend on what happens at run time.

3. The Kinds of Scope

Four kinds, ordered from narrowest outward.

Block scope. A name declared inside { } is visible from its declaration to the closing brace. Chapter 5.1 §5 measured this for a branch, and Chapter 5.5 §2 for a for loop variable — in both cases using the name afterwards was a compile error rather than a warning.

Function scope. A parameter's name is visible throughout the function body — Chapter 6.2 established that a parameter is an ordinary object, and its name behaves like any other local.

Namespace scope. A name declared outside any function or class. The outermost one is the global namespace, and Chapter 7.6 is where namespaces are the subject. For this chapter, what matters is that namespace scope is the outermost region and ::name reaches it explicitly.

Class scope. The frozen curriculum names this and classes are Module 11, so here is the one fact that is true without any class machinery: a class's members are in scope inside that class's own member functions, without qualification. Chapter 11.3 is where that becomes real. Nothing in this chapter depends on it.

The two statement forms from Module 5 are worth collecting here, because they exist precisely to control scope:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    if (int status = read_status(); status != 0) { /* status in scope here */ }
    for (int i = 0; i < n; ++i)                  { /* i in scope here */ }

Both put a name in a scope that ends with the statement. Chapter 5.1 §6 and Chapter 5.5 §2 made the case; this is the reason it is a scope question.

4. The Lookup Rule

Here is the whole of it:

To find which declaration a name refers to, search the innermost scope containing the name. If it is not found there, search the next scope out, and so on. The first declaration found wins; the search then stops.

Three things follow that cover almost every case you will meet:

Inner declarations hide outer ones. Not an error — the search simply stops before it reaches the outer one. §6 is about this.

The search stops at the first hit. It does not collect candidates and choose. (Overload resolution, Chapter 6.7, does choose — but only among declarations found in the same scope by this search.)

Qualification bypasses the search. ::width says "namespace scope, not whatever is nearer", and sim::width says which namespace. That is the escape hatch when the nearest declaration is not the one you want.

5. The Rule, Measured

Three nested scopes, each declaring width:

Azvya Education Pvt. Ltd.VLSI Mentor
lookup.cpp — complete program
#include <iostream>

int width = 1;                       // namespace scope

static void f()
{
    std::cout << "  at function scope, width = " << width << '\n';   // finds ::width
    int width = 2;                                                   // function scope
    std::cout << "  after declaring,  width = " << width << '\n';    // finds the local
    {
        int width = 3;                                               // block scope
        std::cout << "  in inner block,   width = " << width << '\n';
        std::cout << "  ...and ::width  = " << ::width << '\n';
    }
    std::cout << "  back outside,     width = " << width << '\n';
}

int main() { f(); return 0; }

Measured output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  at function scope, width = 1
  after declaring,  width = 2
  in inner block,   width = 3
  ...and ::width  = 1
  back outside,     width = 2

Every line is the rule applied once.

Line 1 prints 1. The function-scope width has not been declared yet at that point, so the search finds ::width. This is the same "declare before use" rule Chapter 6.1 §2 measured for functions, operating at block scope.

Line 2 prints 2. Now the local exists and the search stops there.

Line 3 prints 3. The inner block's declaration is nearer still.

Line 4 prints 1. ::width skips the search entirely.

Line 5 prints 2. The inner block ended, so its width is out of scope and the function-scope one is nearest again.

Zero warnings at -Wall -Wextra -Wpedantic. Nothing here is irregular as far as the language is concerned.

When an inner declaration hides an outer one, the inner name is said to shadow the outer.

Azvya Education Pvt. Ltd.VLSI Mentor
shadow.cpp — complete program
#include <iostream>

int main()
{
    int count = 1;
    {
        int count = 2;                 // hides the outer count
        std::cout << "  inner count = " << count << '\n';
    }
    std::cout << "  outer count = " << count << '\n';
    return 0;
}

Measured output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  inner count = 2
  outer count = 1
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic shadow.cpp -o shadow
$                                            ← zero warnings

Silent at this curriculum's standing flags. The flag exists and is separate:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic -Wshadow shadow.cpp -o shadow
shadow.cpp:6:13: warning: declaration shadows a local variable [-Wshadow]
shadow.cpp:4:9: note: previous declaration is here

Run it over §5's program and it reports two such warnings; without the flag, zero.

This is now a familiar shape. Chapter 5.3 measured that switch fallthrough needs -Wimplicit-fallthrough, and Chapter 5.5 that the unsigned countdown needs -Wtautological-unsigned-zero-compare. Chapter 5.7 §8's callout gave the reason those warnings sit outside -Wall: the construct has a legitimate use spelled identically. Shadowing qualifies — a short inner name reusing an outer one is sometimes exactly what a reader wants, and a warning that fires on every such case would be switched off.

The practical position:

Shadowing a variable in a small scope is usually harmless. A loop index shadowing something three functions out is noise, not a bug.

Shadowing across a long function is where it bites. An edit to the inner declaration silently changes which object a line twenty lines below refers to, and nothing at the usual flags will say so.

-Wshadow is worth enabling on new code and frequently unusable on old code, because it fires on a great deal of existing style. If you can afford it, take it.

7. Two Order Traps

The lookup rule is positional, and that produces two results people do not expect.

A name is not visible before its declaration, even in the same block.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    std::cout << n << '\n';
    int n = 5;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: use of undeclared identifier 'n'

An error, not a warning. Chapter 6.1 §2 measured the same thing for a function whose definition appeared lower in the file; this is the block-scope version of one rule.

A name is in scope inside its own initializer. This is the sharp one.

Azvya Education Pvt. Ltd.VLSI Mentor
selfinit.cpp — complete program
#include <iostream>

int value = 7;                // namespace scope

int main()
{
    int value = value + 1;    // which `value` is on the right?
    std::cout << value << '\n';
    return 0;
}

The intuition is that the right-hand side runs before the local exists, so it should find ::value and give 8. That is wrong, and the compiler says so:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
selfinit.cpp:7:17: warning: variable 'value' is uninitialized when used within its own initialization [-Wuninitialized]

A name's scope begins at its declarator — at the point the name itself appears — not at the end of the statement. So by the time the compiler reads value + 1, the local value is already the nearest declaration, the search stops there, and the program reads an object that has not been initialized yet.

Chapter 3.8 is the chapter on what reading an uninitialized object means: the value is indeterminate and reading it is undefined behaviour. So this line is not merely surprising; it is a bug, and the output is not something to reason about.

What makes it worth a section is that the surprise is entirely explained by the lookup rule. Nothing special is happening — the search started at the innermost scope, as always, and the innermost scope already contained value.

8. What to Actually Do

Declare names in the narrowest scope that works. A name that exists only inside a loop belongs inside the loop — Chapter 5.5 §2.

Use the if and for init-statement forms to put a test-only name in a scope that ends with the statement.

Do not reuse an outer name in an inner scope unless the inner scope is small enough to see whole.

Turn on -Wshadow for new code. It is not in -Wall -Wextra -Wpedantic.

Qualify when the nearest declaration is not the one you want — ::name for namespace scope, sim::name for a namespace.

Never initialize a variable from a name it shadows. Rename one of them; int local_value = value + 1; is unambiguous.

Read a long function with the lookup rule in mind. If a name's meaning is not obvious at a glance, the function is too long or the names are too similar.

9. Mistakes Worth Making Once

Expecting a shadowed name to warn. Measured: zero warnings at -Wall -Wextra -Wpedantic; -Wshadow is separate.

Using a name above its declaration in the same block. error: use of undeclared identifier 'n'.

Writing int value = value + 1; expecting the outer value. Measured: variable 'value' is uninitialized when used within its own initialization — the local is already in scope.

Thinking scope has anything to do with run time. It is a region of source text, settled at compile time.

Thinking scope tells you when an object is destroyed. It does not — Chapter 7.2.

Expecting lookup to consider every candidate. It stops at the first scope that has the name.

Reusing a name across a long function. The one place shadowing genuinely causes bugs.

10. What You Should Be Able to Say Now

  1. Three questions get one word. This chapter is only where a name is visible; 7.2 and 7.3 are lifetime, and 7.5 is linkage.
  2. Scope is a region of source text, settled at compile time, and it is a property of a name rather than an object.
  3. One object can have several names with different scopes — Chapter 6.4's reference parameter.
  4. Four kinds: block, function, namespace (outermost is global), and class — the last deferred to Chapter 11.3.
  5. The lookup rule: search the innermost scope containing the name, then outward; the first declaration found wins and the search stops.
  6. Measured: the same name in three nested scopes printed 1, 2, 3, then ::width gave 1, then 2 again — the rule applied five times.
  7. Inner declarations hide outer ones, which is called shadowing and is legal.
  8. Measured: shadowing is silent at -Wall -Wextra -Wpedantic; -Wshadow reports it (two warnings on §5's program).
  9. That is Chapter 5.7 §8's pattern — the construct has a legitimate use spelled identically, so the warning sits outside -Wall.
  10. A name is not visible before its declaration, even in the same block: error: use of undeclared identifier 'n'.
  11. A name is in scope inside its own initializer. Measured: int value = value + 1; warns -Wuninitialized and does not find ::value.
  12. A name's scope begins at its declarator, which is what makes both traps follow from one rule.
  13. Lookup is positional so the compiler can resolve names in one pass, top to bottom — the same property Chapter 6.1's declare-before-use relies on.
  14. Qualification bypasses the search — ::name, or sim::name once Chapter 7.6 arrives.

11. Exercises

Exercise 1 — which declaration? For §5's program, say which width each of the five output lines refers to, and name the scope of each.

Exercise 2 — predict the output. Give the output if §5's inner block's int width = 3; is deleted, and say which rule changed the answer.

Exercise 3 — name the scope. For each, name the scope of the declared name and where it ends: (a) int i in for (int i = 0; ...) (b) int status in if (int status = f(); status != 0) (c) a parameter int n (d) int width at namespace scope (e) int t inside a bare { } in a function.

Exercise 4 — find the shadow. Write a program where shadowing produces a wrong answer that -Wall -Wextra -Wpedantic does not report, then give the flag that catches it and the compiler's message.

Exercise 5 — explain the trap. int value = value + 1; inside a function that also has a namespace-scope value. Say what the right-hand side refers to, why, what the compiler reports, and which chapter covers what the program then does.

Exercise 6 — fix it three ways. Repair Exercise 5's line so it means "one more than the namespace-scope value", using (a) a different local name, (b) qualification, (c) a different structure. Say which you would ship.

Exercise 7 — three questions. For each statement, say which of §1's three questions it is about: (a) "count is not declared here" (b) "the object still holds its value on the next call" (c) "another source file cannot refer to this function" (d) "the reference points at something destroyed" (e) "the inner declaration hides the outer one".

12. Summary

  • Three questions, one word. This chapter is where a name is visible; lifetime is 7.2–7.3 and linkage is 7.5.
  • Scope is a region of source text and a property of a name, not an object.
  • Four kinds: block, function, namespace, class — class deferred to 11.3.
  • The lookup rule: innermost scope outward, first hit wins, search stops.
  • Measured: 1, 2, 3, ::width = 1, then 2 — five applications of one rule.
  • Shadowing is legal and silent at the standing flags; -Wshadow is a separate flag.
  • A name is invisible above its declaration — an error.
  • A name is in scope inside its own initializer — measured, int value = value + 1; reads itself and warns -Wuninitialized.
  • A name's scope begins at its declarator, which is why both traps are one rule.
  • Lookup is positional so names resolve in one top-to-bottom pass.

Scope is settled entirely at compile time, and that is exactly why it cannot answer the question that matters most about the objects those names refer to. The next chapter is the module's Critical one, and it starts from a pair of facts that look contradictory: a program in which an object keeps its value between calls to a function while its name is a compile error anywhere outside that function, and a program in which two differently-named objects in two different functions turn out to have been at the same address. Neither is unusual, both are measured, and together they establish that where a name can be written and how long an object exists are not merely different questions — they are independent ones, and the answer to one tells you nothing about the other.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.