Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

A Console Program Takes Shape in C++

CarvedRock runs a rental depot that outfits hikers with gear and needs a console program that checks a hiker's gear against their weight allowance before they leave the counter. You will grow the program from a single translation unit into a proper multi-file C 20 project: a catalog module that looks up rental rates, a gear module and a hiker module that construct validated objects inside their own namespaces, a checkout module that classifies the total load with const-correct functions and lambdas, and a final assembled receipt that ties every module together into one running pipeline.

Lab platform
Lab Info
Level
Beginner
Last updated
Nov 10, 2026
Duration
45m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. Challenge

    Step 1: Explore the CarvedRock depot console program

    CarvedRock needs a real console program

    CarvedRock runs a gear depot that rents tents, backpacks, stoves, and boots to hikers heading out for the day. Before anyone leaves the counter, the depot needs to know the daily rate for what they're renting, whether the gear they've picked fits their weight allowance, and what to print on the receipt. Right now that logic is scattered across a single file. Over the next four steps you'll grow it into a proper multi-file C++20 project: a catalog module for rates, a gear module and a hiker module that build validated objects, a checkout module that classifies the load, and a receipt module that ties every piece together into one printed line. ## Reading the starter layout

    Open src/ and you'll find pairs of files: catalog.hpp/catalog.cpp, gear.hpp/gear.cpp, hiker.hpp/hiker.cpp, checkout.hpp/checkout.cpp, receipt.hpp/receipt.cpp, plus a thin main.cpp that will eventually call into all of them. Each .cpp file is its own translation unit: the compiler turns it into an object file entirely on its own, only trusting the matching .hpp file for the shape of anything it doesn't define itself. The linker then stitches every object file together into one executable. This is why a header and its source file must agree exactly on every declaration -- the compiler never cross-checks them against each other until link time, and a mismatch there shows up as an error with no line number in your source at all. ## The checkout pipeline you're building toward

    A checkout run does five things in order: look up the daily rate for a gear category, read the hiker's name and rental duration from the console, construct validated GearItem and Hiker objects, classify the total load against the hiker's weight allowance, and print a receipt. Step 2 wires up the rate lookup and the console input. Step 3 builds the validated gear and hiker objects inside their own namespaces. Step 4 factors the load classification into const-correct functions and a lambda. Step 5 closes the gap between declarations and definitions and sequences everything into one finished run.

    info> If you get stuck, you can refer to the provided solution code for each task, available in the solution folder.

  2. Challenge

    Step 2: Wire translation units and capture hiker input

    Calling across a translation-unit boundary

    catalog.hpp declares depot::catalog::dailyRateFor, and main.cpp will call it having only ever seen that declaration -- the actual lookup logic lives in catalog.cpp, compiled separately. As long as both files agree on the declaration, the linker resolves the call to whichever object file defines the function body. Right now that body is a placeholder, so the rate lookup returns the same value no matter what a hiker rents. ## Initializing a variable at the point you read it

    A console program is only as reliable as the input it captures. Declaring std::string name; and filling it in later leaves a window where name exists but holds nothing meaningful -- and if an early return or exception fires in that window, code downstream sees a variable that was never really set. src/intake.cpp will instead declare each variable immediately before the std::getline call that fills it, rather than earlier and left unset in between, so the variable is valid from the moment it exists. ## Narrowing conversions you can see

    A hiker might type 2.6 for the number of rental days. Converting that double straight to int truncates it to 2, silently shortchanging the depot by most of a day. static_cast<int> doesn't fix that by itself -- it only makes the narrowing explicit in the code instead of letting an implicit conversion do it invisibly. Combined with std::round first, the cast becomes a deliberate, visible rounding step rather than a silent truncation.

  3. Challenge

    Step 3: Construct gear and hiker objects inside collision-free namespaces

    Enforcing invariants in a constructor

    A GearItem with an empty name or a negative weight shouldn't be constructible at all -- checking for that after the fact is easy to forget. depot::gear::GearItem's constructor is the one place every GearItem in the program passes through, so it's the right place to reject bad data once and for all: throw before the object exists rather than letting invalid data spread through the rest of checkout. ## Namespaces keep identical names apart

    depot::gear::describe and a function of the same name in another namespace would be entirely different symbols to the compiler, even though an unqualified describe outside any namespace would be a redefinition error. Namespaces exist precisely to prevent that collision -- which is why depot::gear's own validated classes and depot::roster's Hiker class can each grow independently without ever stepping on each other's names. depot::gear::describe still needs its body. ## The same invariant, a different class

    depot::roster::Hiker needs the same kind of protection as GearItem: a hiker with no name or no weight allowance shouldn't be constructible. The check is different -- a positive weight allowance instead of a positive item weight -- but the shape of the fix, validating inside the constructor before any member is trusted, carries over directly.

  4. Challenge

    Step 4: Factor checkout logic into const-correct functions and lambdas

    Const-correctness as a promise

    totalGearWeight only ever needs to read a hiker's gear list, never change it, and its signature should say so: taking the vector by const& and never calling a non-const method on it is a compiler-checked promise to every caller that their data comes back untouched. It's also the number every other calculation in this step depends on, so getting it right first matters. ## Replacing a loop with std::count_if and a lambda

    Counting items over a threshold with a hand-written loop means a counter variable, a loop, and an if -- three moving parts for one idea. std::count_if collapses that into one call: it walks the range and tallies every element for which a predicate returns true. That predicate is naturally a lambda, written inline where it's used, capturing whatever comparison value it needs from the surrounding scope. ## Turning a number into a category

    A raw kilogram total doesn't mean much on its own -- a hiker needs to know whether their load counts as light, standard, heavy, or overweight. classifyLoad is a small, ordinary branching function, but it's the piece that turns every number the earlier functions computed into a category checkout can reason about.

  5. Challenge

    Step 5: Separate declarations from definitions and assemble the checkout flow

    When a declaration and its definition drift apart

    catalog.hpp and catalog.cpp must agree exactly on every function's parameter types and return type -- not because the compiler checks it while compiling catalog.cpp alone, but because every other file that calls the function only ever sees the header. If the two drift apart, each file can compile perfectly well on its own, and the mismatch only surfaces as an unresolved symbol when the linker tries to connect a call site to a definition that doesn't quite match. lateFeeMultiplier is declared correctly already; its body just doesn't yet do what the declaration promises. ## Making a function callable from outside its own file

    buildReceipt is defined in receipt.cpp, but nothing outside that file has ever been told it exists. A function's declaration in a header is the contract other translation units compile against -- without it, no other file can call the function no matter how correct its definition is. Adding that declaration is a small edit with a large effect: it's what turns a private helper into a piece of the program other files can use. ## Assembling the full run

    Every module up to this point does one job in isolation: catalog prices, gear and hiker validate, checkout classifies. runCheckout is where those jobs get sequenced into a single result -- the rate lookup feeds the total cost that buildReceipt assembles into the final receipt string, while the weight total and load classification exercise the rest of the checkout pipeline along the way.

  6. Challenge

    Step 6: Run the application

    Every task is done, and each one was graded the moment you completed it. What you have not done yet is watch the whole application run at once -- which is the only place the pieces you wrote separately become a single working program.

    Start the application

    Open the Terminal tab and run:

    make app && ./app
    

    The program runs in the terminal and prints its output there. Every behaviour it shows is code you wrote in the previous steps, so read the output against what each task asked for -- that is the whole lab, working as one program. ### Run the checks yourself

    The lab ran each task's test for you as you went, but they are ordinary tests and you can run them whenever you like. The whole suite:

    bash runTest.sh
    

    Or one task at a time:

    bash runTest.sh "task 2.1"
    

    Each failure names the task it belongs to, so the output tells you which step to go back to rather than which line to stare at. Two things worth trying before you finish: break something on purpose and watch the matching check fail, then put it back. Knowing what a failure looks like is worth as much as knowing what a pass looks like, and it costs you nothing here.

    When you are done, everything you wrote is still in the editor tabs -- the lab is yours to keep reading.

About the author

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight