Pass Telemetry
You're building Pass Telemetry — a tiny multi-file library that a (pretend)
LLVM optimization pass uses to keep score. Each time the pass runs over a function
it wants two things: to know how many times it has run so far this process, and
to hand out a fresh unique ID for every counter it inserts. Both need state
that survives between calls — which is the whole reason static storage duration
exists. This is the exact reason real LLVM passes wrap their helpers and counters in
their own namespace with internal linkage.
The library is split the professional way: a header
(telemetry.h) holds the interface — inline constexpr config
constants, a static_assert, and the public API declarations inside
namespace telemetry. You implement the bodies in
starter/telemetry.cpp: an internal-linkage helper hidden
in an unnamed namespace, a function whose static local call-counter climbs
across calls, a static-local unique-ID dispenser, and a function that reaches a
shadowed global with ::. There are no loops (that's Chapter 8) — the tests
make persistence visible by calling your functions several times in a row and
watching the numbers go up.
Your tasks
telemetry::configuredFirstId()— returnconfig::firstCounterId. Trivial, but it makes you reach into the nested config namespace with::.- The global +
globalBuildTag()(two parts, both markedTASK 2) — first define the globalg_buildTag(the header only declared itextern), initialized to6340. Then in the global-namespace functionglobalBuildTag(), declare a local namedg_buildTagthat shadows the global, and return the global one via::g_buildTag. recordRun()+runCount()—recordRun()increments the shareds_runCountand returns the new total; because it's static-duration state, the value persists (call → 1, call → 2, call → 3).runCount()returns it without mutating.nextCounterId()— a unique-ID dispenser built on a static local initialized toconfig::firstCounterId. Hand out1000, 1001, 1002, …, one per call, never repeating.isFirstRunNumber()+isFirstRun()(two parts, both markedTASK 5) — implement the internal helper (runNumber == 1) and haveisFirstRun()call it withs_runCount. Before any run is recorded it must returnfalse.
Success criteria
make test prints PASS ✅ all checks. The grader links
tests/tests.cpp against your starter/telemetry.cpp and, among
other things, calls recordRun() three times expecting 1, 2, 3, calls
nextCounterId() three times expecting 1000, 1001, 1002, and checks the
edge case that before any run runCount() == 0 and isFirstRun() == false.
Until you implement the tasks the placeholders return 0/false and make test
shows FAIL ❌ with the exact CHECK lines that failed. Turning that red into
green is the whole exercise.
A good gut-check: make run (your code) vs make solution (reference). Yours prints
all zeros; the reference prints the counter climbing 1 → 2 → 3 and IDs
1000 → 1001 → 1002. When your make run matches the reference, you're done.
Concepts practiced
- User namespaces and the scope-resolution operator
::, incl. nestedtelemetry::config(7.2) - Static local variables: block scope but static duration — they remember across calls (7.11)
- Internal linkage via an unnamed (anonymous) namespace for file-local helpers (7.6, 7.14)
- External linkage: a non-const global declared
externin the header, defined once in a.cpp(7.7) - Variable shadowing and reaching the global with the
::nameform (7.5) inline constexprnamespace constants shared across translation units, the C++17 way (7.9, 7.10)static_assertas a compile-time guard — a preview of Chapter 9 (notes 9.6); it's provided in the header, you don't write one- Reused from earlier chapters: multi-file build, headers + header guards, declarations vs definitions (Ch 2);
std::string_viewandconstexpr(Ch 5); the?:/relational feel for tiny predicates (Ch 6)
Constraints
- Edit only
starter/telemetry.cpp. Do not changetelemetry.h,demo.cpp, ortests/. - The internal helper and
s_runCountmust stay inside the unnamed namespace (internal linkage) — do not move them intonamespace telemetryor make them part of the public API. recordRun()andnextCounterId()must use static-duration state, not a plain (automatic) local — otherwise the value resets every call and the persistence checks fail.- The global
g_buildTagmust be defined exactly once (here), matching the header'sexterndeclaration. - No loops, no
if-as-loop tricks, no<random>(Chapter 8). Observe persistence through the tests' repeated calls. No mutable globals beyond the one taught here. - Stays warning-clean under
clang++ -std=c++17 -Wall -Wextra.
Build & run locally
make # compile starter library + demo -> starter/app
make run # run the demo against YOUR library (placeholders print zeros)
make test # grade your code (RED until you fill in the tasks)
make solution # build + run the demo against the REFERENCE (numbers climb)
make cleanHints
Task 1 — reaching a nested namespace
config is nested inside telemetry, and you're already inside namespace telemetry when you write the body, so the constant is just config::firstCounterId.
(From outside it would be telemetry::config::firstCounterId.) The :: operator
reads as "look inside the left-hand scope for the right-hand name."
Task 2 — define the global, then shadow it
Part A is one line at file scope: int g_buildTag { 6340 }; — that's the single
definition the header's extern int g_buildTag; promised. Part B: inside
globalBuildTag(), int g_buildTag { -1 }; creates a local that hides the
global; return ::g_buildTag; — the leading :: means "the global namespace," so it
skips your local and returns 6340.
Task 3 — static local vs ordinary local
s_runCount already lives in the unnamed namespace (static duration). recordRun()
is just ++s_runCount; return s_runCount;. The key idea: because it's static, it
is not recreated each call — its value carries over. runCount() is return s_runCount; with no ++. (If you instead wrote int s_runCount { 0 }; inside
recordRun, it would reset to 0 every call — try it to see the difference, then put
it back.)
Task 4 — a static local that remembers
static int s_nextId { config::firstCounterId }; // runs ONCE, the first call only
return s_nextId++; // return current value, THEN advanceThe static is what makes the initialization happen once and the value persist.
Post-increment (s_nextId++) returns the value before adding 1, so the first
call yields firstCounterId, the next +1, and so on.
Task 5 — use the internal helper
The helper is one line: return runNumber == 1;. Then isFirstRun() is return isFirstRunNumber(s_runCount);. Before any recordRun(), s_runCount is 0, so
isFirstRunNumber(0) is false — which is exactly the behavior the edge-case check
wants. Delete the (void)…; placeholder lines once your real code uses the names.
Stretch goals
- Add
telemetry::resetRuns()that sets the internal counter back to 0, and a test that records, resets, then records again. (Notice how hidden the state is — only this.cppcan touch it.) - Make
nextCounterId()configurable with a stride (config::idStride) so IDs go1000, 1010, 1020, …. Add aninline constexpr int idStrideto the config and astatic_assert(idStride > 0, …). - Move the file-local helper from an unnamed namespace to a
staticfree function and confirmmake teststill passes — then read 7.14 on why unnamed namespaces scale better for groups of helpers and for user-defined types. - Add a second telemetry
.cpp(a different pass) with its owns_runCountand prove the two counters are independent — the payoff of internal linkage. (Wiring a second translation unit edges into Chapter 2 multi-file territory; fine to explore.)
// starter/telemetry.cpp — YOU implement the Pass Telemetry library here.
//
// This file already COMPILES (every function returns a placeholder), but the
// placeholders are wrong, so `make test` is RED. Fill in the five TASK blocks to
// turn it GREEN. Build / grade with:
// make compile the starter library + a tiny demo
// make test grade your code (red until you implement the tasks)
// make solution run the reference if you get stuck
//
// This is the IMPLEMENTATION translation unit; it #includes its paired header so
// the compiler checks your definitions against the promised declarations.
#include "../telemetry.h"
// ─── TASK 2 (part A): define the global ────────────────────────────────────
// The header DECLARED `extern int g_buildTag;` — a promise that the object exists
// somewhere. Exactly ONE .cpp must DEFINE it. Define it here and initialize it to
// 6340. (Notes 7.7: non-const globals have external linkage; one definition only.)
//
// >>> YOUR CODE HERE <<<
int g_buildTag {}; // <-- replace the {} (zero) with the real value 6340
// ───────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// Internal-linkage helpers live in this UNNAMED (anonymous) namespace: visible in
// THIS file only, invisible to the rest of the program. (Notes 7.6 / 7.14.)
// s_runCount is the pass's private run-counter — namespace-scope, STATIC duration
// (lives the whole program), INTERNAL linkage. It is provided for you; recordRun()
// will bump it and runCount()/isFirstRun() will read it.
// ─────────────────────────────────────────────────────────────────────────────
namespace
{
int s_runCount { 0 };
// ─── TASK 5 (part A): internal helper ──────────────────────────────────
// Return true exactly when `runNumber` is the FIRST run (i.e. equals 1).
// It stays in this anonymous namespace on purpose: it is an implementation
// detail, not part of the library's public interface.
//
// >>> YOUR CODE HERE <<<
bool isFirstRunNumber(int runNumber)
{
(void)runNumber; // remove this line once you use the parameter
return false; // <-- replace with the real check
}
// ───────────────────────────────────────────────────────────────────────
}
namespace telemetry
{
// ─── TASK 1: configuredFirstId ─────────────────────────────────────────
// Return the shared config constant config::firstCounterId. Reach into the
// nested namespace with the scope-resolution operator `::`. (Notes 7.2.)
//
// >>> YOUR CODE HERE <<<
int configuredFirstId()
{
return 0; // <-- replace with config::firstCounterId
}
// ───────────────────────────────────────────────────────────────────────
// ─── TASK 3: recordRun + runCount ──────────────────────────────────────
// recordRun(): increment the shared s_runCount and return the NEW total. Since
// s_runCount has static duration, the count must PERSIST across calls — call
// once → 1, again → 2, again → 3. runCount(): return s_runCount WITHOUT
// changing it. (Notes 7.11.)
//
// >>> YOUR CODE HERE <<<
int recordRun()
{
(void)s_runCount; // placeholder: delete once you really use s_runCount
return 0; // <-- bump s_runCount, then return it
}
int runCount()
{
return 0; // <-- return s_runCount (no mutation)
}
// ───────────────────────────────────────────────────────────────────────
// ─── TASK 4: nextCounterId ─────────────────────────────────────────────
// A unique-ID dispenser. Use a STATIC LOCAL initialized to config::firstCounterId
// the first time control reaches it; then hand out firstCounterId, +1, +2, ...
// (one per call, never repeating). Hint: `static int s_nextId { ... };` plus
// post-increment `s_nextId++` returns the current value then advances. (Notes 7.11.)
//
// >>> YOUR CODE HERE <<<
int nextCounterId()
{
return 0; // <-- use a static local; return then advance
}
// ───────────────────────────────────────────────────────────────────────
// ─── TASK 5 (part B): isFirstRun ───────────────────────────────────────
// Report whether the current run is the pass's first, by passing s_runCount to
// your internal helper isFirstRunNumber(). Before any run is recorded
// (s_runCount == 0) this must return false.
//
// >>> YOUR CODE HERE <<<
bool isFirstRun()
{
(void)isFirstRunNumber; // placeholder: delete once you call the helper
return false; // <-- return isFirstRunNumber(s_runCount)
}
// ───────────────────────────────────────────────────────────────────────
}
// ─── TASK 2 (part B): shadowing + the `::` global form ──────────────────────
// This function lives in the GLOBAL namespace (note: NO `telemetry::`). Inside it,
// declare a LOCAL variable named `g_buildTag` (any int value) — it will SHADOW the
// global one. Then return the GLOBAL `g_buildTag` using `::g_buildTag`. The whole
// point: prove you can reach past a shadowing local to the global. (Notes 7.5 / 7.2.)
//
// >>> YOUR CODE HERE <<<
int globalBuildTag()
{
return 0; // <-- declare a local g_buildTag that shadows, then return ::g_buildTag
}
// ───────────────────────────────────────────────────────────────────────────
Try the lab first — the learning is in the attempt.
// solution/telemetry.cpp — REFERENCE implementation of the Pass Telemetry library.
//
// One correct way to do it. Peek only if you're stuck; you learn far more by
// turning `make test` from red to green on your own first.
//
// This is the IMPLEMENTATION translation unit. It #includes its own header so the
// compiler can check every definition matches the promised declaration.
#include "../telemetry.h"
// ─────────────────────────────────────────────────────────────────────────────
// TASK 2 (part A) — the GLOBAL definition.
//
// The header DECLARED `extern int g_buildTag;` (a promise). Exactly one .cpp must
// DEFINE it (create the object). This is that file. Non-const globals have
// external linkage, so this single definition is the one symbol the whole program
// shares. (Notes 7.7.)
// ─────────────────────────────────────────────────────────────────────────────
int g_buildTag { 6340 };
// ─────────────────────────────────────────────────────────────────────────────
// Internal-linkage helpers, hidden in an UNNAMED (anonymous) namespace. Names in
// here are usable in THIS translation unit only — other .cpp files can't see them
// and can't collide with them. This is the modern replacement for file-scope
// `static` helpers. (Notes 7.6 / 7.14.) In real LLVM passes, the pass's private
// helpers live here too.
// ─────────────────────────────────────────────────────────────────────────────
namespace
{
// The pass's private run-counter. It is a NAMESPACE-SCOPE variable with
// STATIC duration (lives for the whole program) and INTERNAL linkage (this TU
// only). recordRun() bumps it; runCount()/isFirstRun() read it.
//
// Could this be a static local inside recordRun()? Yes — but then runCount()
// and isFirstRun() couldn't see it. Sharing it across a few functions in ONE
// file, while keeping it invisible to the rest of the program, is exactly what
// an internal-linkage namespace variable is for.
int s_runCount { 0 };
// Internal helper: is the given run number the very first run?
bool isFirstRunNumber(int runNumber)
{
return runNumber == 1;
}
}
namespace telemetry
{
// ── TASK 1 ───────────────────────────────────────────────────────────────
// Reach into the nested config namespace with `::`.
int configuredFirstId()
{
return config::firstCounterId;
}
// ── TASK 3 ───────────────────────────────────────────────────────────────
// Bump the shared internal counter and return the new total. Because
// s_runCount has STATIC duration, its value survives between calls: the tests
// call recordRun() repeatedly and watch 1, 2, 3, ... climb.
int recordRun()
{
++s_runCount;
return s_runCount;
}
// Read-only peek at the counter — no mutation.
int runCount()
{
return s_runCount;
}
// ── TASK 4 ───────────────────────────────────────────────────────────────
// A unique-ID dispenser using a STATIC LOCAL. s_nextId is initialized ONCE
// (the first time control reaches this line) to the configured base, then
// retains its value between calls. Post-increment returns the current value
// and then advances, so IDs come out as firstCounterId, +1, +2, ... with no
// repeats. The NAME s_nextId is visible only inside this function (block
// scope) — perfect encapsulation of remembered state.
int nextCounterId()
{
static int s_nextId { config::firstCounterId };
return s_nextId++;
}
// ── TASK 5 ───────────────────────────────────────────────────────────────
// Use the internal-namespace helper to classify the current run. Before any
// run is recorded, s_runCount is 0 and isFirstRunNumber(0) is false.
bool isFirstRun()
{
return isFirstRunNumber(s_runCount);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// TASK 2 (part B) — the global-namespace function that demonstrates SHADOWING.
//
// Inside this function we declare a LOCAL named `g_buildTag`. From here on, the
// unqualified name `g_buildTag` means the LOCAL (it shadows / hides the global).
// To reach the GLOBAL one we prefix it with `::` (the global-namespace form of
// scope resolution). (Notes 7.2 / 7.5.)
// ─────────────────────────────────────────────────────────────────────────────
int globalBuildTag()
{
int g_buildTag { -1 }; // a local that SHADOWS the global ::g_buildTag
(void)g_buildTag; // it exists only to prove the shadow; silence "unused"
return ::g_buildTag; // `::` skips the local and reaches the GLOBAL (6340)
}
// tests/tests.cpp — the automated grader for the Pass Telemetry library.
//
// Tiny no-framework harness (same style as the other drills). It is linked against
// EITHER starter/telemetry.cpp (`make test`) or solution/telemetry.cpp
// (`make test-solution`) — see the Makefile.
//
// The chapter's big idea is STATIC DURATION: a static variable REMEMBERS its value
// between calls. We can't write a loop yet (Chapter 8), so we make persistence
// visible the honest way — by CALLING the functions several times in a row and
// asserting the numbers climb. If recordRun()/nextCounterId() used an ordinary
// (automatic) local, every call would reset and these checks would fail.
#include <iostream>
#include <string_view>
#include "../telemetry.h"
static int fails = 0;
#define CHECK(cond) do { if(!(cond)){ std::cerr << "FAIL: " #cond " @line " << __LINE__ << "\n"; ++fails; } } while(0)
int main()
{
// ── Compile-time config contract (Task: header static_assert / inline constexpr).
// These are checked by the COMPILER; if the constants were wrong this test file
// would not even build. We mirror the key invariant here for documentation.
static_assert(telemetry::config::firstCounterId >= 0,
"config base id must be non-negative");
static_assert(telemetry::config::passName.size() > 0,
"pass name should not be empty");
// ── TASK 1: configuredFirstId reaches the nested config constant via `::`.
CHECK(telemetry::configuredFirstId() == 1000);
// and it must actually equal the shared constant, not a hard-coded duplicate:
CHECK(telemetry::configuredFirstId() == telemetry::config::firstCounterId);
// ── TASK 2: shadowing — globalBuildTag() returns the GLOBAL ::g_buildTag (6340),
// NOT the local that shadows it.
CHECK(globalBuildTag() == 6340);
CHECK(globalBuildTag() == g_buildTag); // g_buildTag here is the global object
// ── TASK 3 (+5): EDGE CASE — before any run is recorded, the static counter is
// still at its initial 0, and "is this the first run?" is false (run #1 hasn't
// happened yet). This catches the classic bug of seeding the counter at 1.
CHECK(telemetry::runCount() == 0);
CHECK(telemetry::isFirstRun() == false);
// ── TASK 3: PERSISTENCE across calls. The repetition that a loop would do is
// done here by hand, on purpose. Each call must return the NEW running total.
CHECK(telemetry::recordRun() == 1); // 1st run
CHECK(telemetry::isFirstRun() == true); // now run #1 → first run
CHECK(telemetry::recordRun() == 2); // 2nd run (proves the value SURVIVED)
CHECK(telemetry::recordRun() == 3); // 3rd run
CHECK(telemetry::isFirstRun() == false); // no longer the first run
// runCount() is a read-only peek: calling it must NOT advance the counter.
CHECK(telemetry::runCount() == 3);
CHECK(telemetry::runCount() == 3); // called twice; still 3 → no mutation
// ── TASK 4: the unique-ID dispenser is INDEPENDENT static state. Its first
// output is the configured base, then it climbs by exactly 1 each call.
int id0 { telemetry::nextCounterId() };
int id1 { telemetry::nextCounterId() };
int id2 { telemetry::nextCounterId() };
CHECK(id0 == telemetry::config::firstCounterId); // first ID == 1000
CHECK(id1 == 1001);
CHECK(id2 == 1002);
// EDGE CASE — uniqueness: consecutive IDs must differ (never hand the same ID
// out twice) and increase by exactly one.
CHECK(id0 != id1);
CHECK(id1 != id2);
CHECK((id1 - id0) == 1);
CHECK((id2 - id1) == 1);
// The ID dispenser and the run counter are SEPARATE statics: dispensing IDs
// must not have disturbed the run count (still 3 from above).
CHECK(telemetry::runCount() == 3);
if (!fails) std::cout << "PASS ✅ all checks\n";
return fails ? 1 : 0;
}
// demo.cpp — a tiny program that USES the Pass Telemetry library so you can watch
// the static-duration state with your own eyes. You don't edit this; it's the
// "consumer" translation unit. `make run` builds it against your starter library;
// `make solution` builds it against the reference. It's the same API the tests use,
// just printed instead of asserted.
//
// Notice there are NO loops here — we observe persistence by CALLING the functions
// several times in a row, exactly as the grader does.
#include <iostream>
#include "telemetry.h"
int main()
{
std::cout << "pass name : " << telemetry::config::passName << '\n';
std::cout << "configured firstId: " << telemetry::configuredFirstId() << '\n';
std::cout << "global build tag : " << globalBuildTag() << '\n';
std::cout << "before any run : runCount=" << telemetry::runCount()
<< " isFirstRun=" << std::boolalpha << telemetry::isFirstRun() << '\n';
// Repeated calls — the static counter REMEMBERS between them.
std::cout << "recordRun() -> " << telemetry::recordRun() << '\n'; // 1
std::cout << "recordRun() -> " << telemetry::recordRun() << '\n'; // 2
std::cout << "recordRun() -> " << telemetry::recordRun() << '\n'; // 3
std::cout << "after 3 runs : runCount=" << telemetry::runCount()
<< " isFirstRun=" << telemetry::isFirstRun() << '\n';
// The unique-ID dispenser: a separate static, climbing from the configured base.
std::cout << "nextCounterId() -> " << telemetry::nextCounterId() << '\n'; // 1000
std::cout << "nextCounterId() -> " << telemetry::nextCounterId() << '\n'; // 1001
std::cout << "nextCounterId() -> " << telemetry::nextCounterId() << '\n'; // 1002
return 0;
}
# Chapter 7 — Pass Telemetry · unit-test grader (Style B).
# Multi-file: telemetry.h (interface) + telemetry.cpp (impl) + demo.cpp (consumer).
# Targets follow the drills/CLAUDE.md Makefile contract. Recipes use TABS.
CXX := clang++
CXXFLAGS := -std=c++17 -Wall -Wextra
.PHONY: all build run test solution test-solution clean
all: build
# Build the STARTER library together with the demo consumer (two TUs, then linked).
build: starter/app
starter/app: starter/telemetry.cpp demo.cpp telemetry.h
$(CXX) $(CXXFLAGS) demo.cpp starter/telemetry.cpp -o starter/app
run: build
./starter/app
# Grade the LEARNER's code: link the tests against the starter library.
test: tests/run-starter
@./tests/run-starter || echo "FAIL ❌ fill in the TASK blocks until every CHECK passes."
tests/run-starter: tests/tests.cpp starter/telemetry.cpp telemetry.h
$(CXX) $(CXXFLAGS) tests/tests.cpp starter/telemetry.cpp -o tests/run-starter
# Build + run the REFERENCE solution against the same demo.
solution: solution/app
./solution/app
solution/app: solution/telemetry.cpp demo.cpp telemetry.h
$(CXX) $(CXXFLAGS) demo.cpp solution/telemetry.cpp -o solution/app
# Proof the exercise is solvable: the reference must pass every check.
test-solution: tests/run-solution
@./tests/run-solution
tests/run-solution: tests/tests.cpp solution/telemetry.cpp telemetry.h
$(CXX) $(CXXFLAGS) tests/tests.cpp solution/telemetry.cpp -o tests/run-solution
clean:
rm -f starter/app solution/app tests/run-starter tests/run-solution
// telemetry.h — the public INTERFACE of the Pass Telemetry library.
//
// This header is COMPLETE. You don't edit it — you implement the bodies it
// promises (in starter/telemetry.cpp). Read it top-to-bottom: almost every idea
// in Chapter 7 is sitting right here, labelled.
//
// The story: imagine a (pretend) LLVM optimization pass. Each time it runs over a
// function it wants to (a) know how many times it has run so far this process,
// and (b) hand out a fresh unique integer ID for each counter it inserts. Both of
// those need state that SURVIVES BETWEEN CALLS — exactly what a `static` local
// gives you. The tests will call your functions several times and watch the
// numbers climb. (No loops — Chapter 8 — the *repetition lives in the tests*.)
#ifndef TELEMETRY_H // ── header guard ──────────────────────────────
#define TELEMETRY_H // include this file's contents at most once per TU
#include <string_view> // std::string_view — for compile-time string labels
// ─────────────────────────────────────────────────────────────────────────────
// Shared compile-time CONFIG, the C++17 way: `inline constexpr` in a namespace
// in a header. (Notes 7.9 / 7.10.)
//
// - `constexpr` → a true compile-time constant (usable in static_assert, array
// sizes, etc.).
// - `inline` → "multiple identical definitions across translation units are
// allowed" — so EVERY .cpp that #includes this header shares ONE
// entity (external linkage), instead of each getting a private
// copy. This is the modern fix for sharing constants.
//
// A plain `constexpr` global (without `inline`) has INTERNAL linkage — fine, but
// each TU gets its own copy. `inline constexpr` is the preferred shared form.
// ─────────────────────────────────────────────────────────────────────────────
namespace telemetry::config
{
// The first unique counter ID we hand out. IDs start here and climb.
inline constexpr int firstCounterId { 1000 };
// A human-readable name for this pass, shared by every file that includes us.
inline constexpr std::string_view passName { "cs6340-counter-pass" };
// Compile-time sanity check (a PREVIEW of Chapter 9's static_assert, notes
// 9.6): if someone "fixes" the base ID to a negative number, refuse to
// compile rather than hand out nonsense IDs later.
// static_assert is checked by the COMPILER — a failure is a build error, not a
// runtime surprise. This line needs `firstCounterId` to be a constexpr value;
// that's exactly why config constants are constexpr.
static_assert(firstCounterId >= 0, "firstCounterId must be non-negative");
}
// ─────────────────────────────────────────────────────────────────────────────
// The public API lives in `namespace telemetry`. Functions have EXTERNAL linkage
// by default, so these declarations in the header connect to the definitions you
// write in telemetry.cpp — the linker wires the two translation units together.
// (Notes 7.7.) C++17 lets us spell the nested namespace compactly with `::`.
// ─────────────────────────────────────────────────────────────────────────────
namespace telemetry
{
// TASK 1 — return config::firstCounterId. Trivial, but it makes you reach into
// a nested namespace with the scope-resolution operator `::`.
int configuredFirstId();
// TASK 3 — the heart of the chapter. Increments a STATIC LOCAL counter and
// returns the new running total. Call it once → 1. Call it again → 2. The
// count must PERSIST across calls (static duration), even though the variable's
// NAME is visible only inside the function body (block scope).
int recordRun();
// TASK 3 (companion) — read the current run count WITHOUT changing it.
int runCount();
// TASK 4 — a unique-ID dispenser. Returns config::firstCounterId on the first
// call, then firstCounterId+1, +2, ... Each ID is handed out exactly once.
int nextCounterId();
// TASK 5 — for an active run (recordRun() already called at least once), report
// whether this is the pass's "first run" (run #1) using the internal helper.
// Returns false before any run has been recorded.
bool isFirstRun();
}
// A deliberately-named GLOBAL variable in the GLOBAL namespace, with external
// linkage. The library's `g_buildTag` and a same-named local inside one of your
// functions will collide by name — Task 2 shows you how `::` reaches past a
// shadowing local to the global. (Notes 7.5 / 7.2.) Defined once in telemetry.cpp.
extern int g_buildTag;
// TASK 2 — lives in the GLOBAL namespace (no `telemetry::`). It intentionally
// declares a LOCAL variable named `g_buildTag` that SHADOWS the global above, then
// must return the GLOBAL one via `::g_buildTag`. Proves you understand shadowing.
int globalBuildTag();
#endif // TELEMETRY_H
make test locally
(see “Build & run locally” above).