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

Safe Ownership and Runtime Polymorphism in C++

Meridian Field Station runs a scattered fleet of environmental sensors (thermometers, hygrometers) that report back to a base controller over intermittent links. The existing station code was written years ago with raw arrays of Sensor*, manual new/delete, static_cast downcasts to reach thermometer-only calibration, and exceptions thrown for routine sensor timeouts -- and it crashes in the field with use-after-free and out-of-bounds reads. You are modernizing this codebase step by step: first making failures explicit and recoverable, then fixing the raw-pointer bugs, then moving ownership into RAII and smart pointers, and finally wiring a safe polymorphic fleet controller.

Lab platform
Lab Info
Level
Beginner
Last updated
Nov 07, 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: Orientation to the Meridian Field Station codebase

    Meridian Field Station

    The station's controller code has been running unmodified for years. It works, mostly — until a sensor drops off the link, or two requests race on the same slot, and the station either crashes or silently reports nonsense. Over the next four steps you will rebuild the pieces that fail in the field: how a reading reports failure, how the sensor registry owns its sensors, and how the fleet controller talks to every sensor type without corrupting memory.

    Nothing here is a rewrite from scratch. You are editing the station's existing files in place, the way you would on a real legacy codebase — fixing the bug in front of you before reaching for the next abstraction. ## What's actually wrong

    Four smells run through this codebase, and each maps directly onto a step ahead:

    • Exceptions for routine failures. A sensor timeout — which happens on a bad link several times an hour — is currently thrown as a C++ exception and caught (or not) several call frames away. Step 2 replaces that with an explicit result the caller checks, reserving exceptions for the one failure the station truly cannot start with: a nonsensical calibration config.
    • Raw Sensor* array with hand-rolled bookkeeping. The registry's findSensorByName, removeSensor, and getSensorAt each trust an index or a count that isn't always right, which is exactly how you get a read past the end of the array or a dereference of a pointer that was already deleted. Step 3 fixes each of these in place.
    • Manual new/delete and a raw connection handle. A ConnectionHandle is opened and closed by hand at each call site, and the sensor registry allocates with new and hopes every path remembers to delete. Step 4 moves both into RAII and Rule-of-Zero containers so the compiler enforces cleanup instead of a comment reminding a future maintainer.
    • static_cast used as a shortcut past the type system. The calibration routine assumes every sensor in the fleet is a Thermometer and static_casts to reach calibrate(), which is undefined behavior the moment a Hygrometer is in the mix. Step 5 wires the fleet controller through the sensors' shared virtual interface and swaps that static_cast for a checked dynamic_cast. ## Where this lands

    By the end of step 5 you will have a small but complete FleetController that polls every registered sensor through its virtual interface, calibrates the thermometers among them without disturbing the rest of the fleet, and folds both outcomes into one station report — all without a single raw new, an unchecked cast, or an exception thrown for something as ordinary as a dropped link.

    The fixes are cumulative: the registry you repair in step 3 is the same registry you convert to owning smart pointers in step 4, and the sensors you give a safe virtual interface in step 5 are the ones step 2 taught to report their own failures. Read each file as you reach it — the bug or the gap is usually a handful of lines away from where the TODO sits.

    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: Build the foundational sensor-reading result reporting

    Two kinds of failure

    A sensor timeout is routine — it happens whenever the radio link is briefly down — and the caller needs to know about it without paying for a C++ exception on the common path. A bad calibration config, by contrast, means the station should not start at all: there is no reasonable way to "recover" from a negative scale factor mid-run.

    src/sensor_reading.cpp separates the two. ReadingResult is a small value type with ok(), a .value, and an error code, built with the ReadingResult::success(...) / ReadingResult::failure(...) factories already declared for you. A function that can fail routinely returns one of these instead of throwing. A function that guards a precondition the whole station depends on — like the calibration config it starts with — still throws, because that failure is not something a caller should be expected to check for on every call. ## Keeping the exception path rare

    With readSensorValue now reporting timeouts explicitly, the only throw left in this file should be for a failure the station cannot run with at all. That is exactly what validateCalibrationConfig guards: it runs once, at startup, before a single sensor is polled, so throwing there costs nothing and stops a misconfigured station before it does any damage.

  3. Challenge

    Step 3: Expose and fix raw pointer lifetime failures in the legacy sensor array

    The legacy registry

    src/sensor_registry.cpp still owns its sensors the way it did years ago: a fixed-size array of Sensor* and an int count_ tracking how many slots are actually in use. Three of its methods trust that bookkeeping in a way that occasionally isn't true — a loop bound that doesn't match count_, a delete that leaves its pointer where the next lookup will find it, and an index that's never checked against anything before it's dereferenced.

    You will fix each one where it stands, without changing the array yet — that conversion comes in step 4. For now, the goal is simply that every method respects the same invariant: only the first count_ slots of sensors_ hold a sensor that's still alive. ## Deleting without dangling

    findSensorByName now trusts count_ to mark the boundary of live sensors. removeSensor is the method responsible for keeping that boundary honest: when it deletes a sensor, it has to make sure nothing later in the array — or in count_ itself — still points at, or counts, the slot it just freed. ## The last unchecked access

    Even with the array kept contiguous, getSensorAt still takes an arbitrary caller-supplied index and dereferences sensors_[index] without asking whether that index is one it has ever assigned. This is the same class of bug as the out-of-bounds read in findSensorByName, just reached through a public accessor instead of a search loop — and it gets the same fix: check the bound before you trust it.

  4. Challenge

    Step 4: Move sensor ownership into RAII and Rule-of-Zero containers

    RAII: tying a resource to an object's lifetime

    A raw connection handle that's opened in one function and closed in another only stays balanced as long as every code path between them remembers to close it — including the ones that return early or throw. RAII removes that obligation by tying the resource to an object: the constructor acquires it, the destructor releases it, and the object's own lifetime — which the compiler tracks for you — decides when that happens.

    src/connection_handle.cpp declares ConnectionHandle with its copy operations already deleted, because a handle should have exactly one owner at a time. What's missing is the constructor and destructor that actually acquire and release it, and the move operations that transfer ownership from one ConnectionHandle to another without ever closing the same handle twice. ## Rule of Zero

    A class that manages a resource through a member that already manages itself — a std::vector, a std::unique_ptr — doesn't need a custom destructor, copy constructor, or move constructor at all: the compiler-generated ones already do the right thing, because they just call the same operations on each member. That's the Rule of Zero, and it's the opposite of the fixed Sensor* sensors_[kMaxSensors] array you've been patching in step 3.

    addSensor is the one method left that still needs to change: it should take ownership of a new sensor through a unique_ptr parameter and release it into the array, instead of the caller managing a raw new/delete itself. ## Shared ownership

    std::unique_ptr says "exactly one owner." Sometimes that's wrong on purpose — several sensors legitimately need to read the same calibration data, and none of them should have to duplicate it just to hold a reference. std::shared_ptr is for that case: copying one doesn't copy the pointee, it increments a shared reference count, and the pointee is only destroyed once every shared_ptr to it is gone.

    src/calibration_profile.cpp declares CalibrationProfile and a Sensor::profile_ member of type std::shared_ptr<CalibrationProfile>. attachSharedProfile is the function that hands the same profile to every sensor in a fleet.

  5. Challenge

    Step 5: Wire the fleet controller through virtual interfaces safely

    Virtual dispatch without slicing

    Sensor declares read() as virtual, and Thermometer and Hygrometer each override it with their own conversion logic. Calling that override safely depends on going through a pointer or reference to the base class — sensor->read() on a Sensor* or unique_ptr<Sensor> — rather than through a Sensor held by value. Assigning a Thermometer into a Sensor variable copies only the base-class part of it, discarding the derived data and the override along with it; that's slicing, and it's why the fleet is stored as std::vector<std::unique_ptr<Sensor>> rather than a vector of Sensor values.

    src/fleet_controller.cpp is where the fleet controller lives. pollFleet is its first job: read every sensor in the fleet through that shared interface. ## dynamic_cast instead of static_cast

    static_cast to a derived type trusts the caller completely: if the object underneath isn't actually that derived type, the cast still "succeeds" and produces a pointer to memory that was never laid out the way the derived class expects. dynamic_cast checks the object's real type at runtime and returns nullptr when the cast doesn't hold — which is exactly the check you need before calling a method, like Thermometer::calibrate, that only thermometers have.

    calibrateThermometers walks the same fleet pollFleet just read, but only some of those sensors are thermometers. ## Bringing it together

    pollFleet and calibrateThermometers each do one job well. generateStationReport is where those two results become the single piece of text the rest of the station actually consumes once per cycle — every sensor's reading or failure reason, and a note on how many thermometers were just calibrated.

  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