Skip to Content
Course content

28: Inheritance and Access Control

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first encounter inheritance, the goal usually seems simple: you have a base class with some common logic, and you want a derived class to reuse that logic. The struggle starts when the derived class needs to touch the data inside the base class. I've seen a lot of developers fall into a trap here where they treat access modifiers as a nuisance to be bypassed rather than a tool for design.

The temptation of the public floodgate

Imagine we're building a simple game engine. We have a GameObject class that tracks a position in a 2D world. The naive approach is to make everything public just so the subclasses—like a Player or an Enemy—can easily update their coordinates. It looks something like this:

class GameObject {
public:
    float x, y;
    int id;
};

class Player : public GameObject {
public:
    void move(float dx, float dy) {
        x += dx;
        y += dy;
    }
};

On the surface, this works. It's concise. But as a software engineer, you have to ask: who else can see x and y? In this setup, any part of your program—a UI script, a physics solver, or a random sound manager—can reach into your Player object and teleport them to coordinates (9999, 9999) without the Player class ever knowing. You've traded encapsulation for a tiny bit of convenience in the subclass. You've basically told the rest of the world, "I don't care who touches my internals."

The "Family Secret" approach with protected

You might then think, "Okay, I'll just make them private." But the moment you do that, your Player::move method will throw a compiler error. C++ is strict: private means private, even to your own children. This is where protected comes in. I like to think of protected as a family secret: the general public can't know it, but the kids inherit the knowledge.

class GameObject {
protected:
    float x, y;
    int id;

public:
    GameObject(int id) : id(id), x(0), y(0) {}
};

class Player : public GameObject {
public:
    Player(int id) : GameObject(id) {}
    void move(float dx, float dy) {
        x += dx; // This works now!
        y += dy;
    }
};

This is significantly better because the GameObject's position is now hidden from the rest of the application, but the Player can still manipulate it. However, there's a hidden cost here that I want you to be aware of: the Fragile Base Class problem. When you make data protected, you're creating a tight coupling between the base and the derived class. If you later decide that x and y should be replaced by a Vector2 object to support better math, you have to hunt down every single subclass that touched those floats and rewrite them. You've leaked the implementation details of the base class into the children.

Finding the balance with controlled access

The professional way to handle this is to keep your data private and provide protected methods for modification. This gives the base class a "gatekeeper" role. Instead of letting the Player class just overwrite the coordinates, you give them a method that can include validation—like checking for map boundaries.

class GameObject {
private:
    float x, y;
    int id;

protected:
    void setPosition(float newX, float newY) {
        // I can add logic here to ensure the object doesn't leave the map
        x = newX;
        y = newY;
    }

public:
    GameObject(int id) : id(id), x(0), y(0) {}
    void getPosition() const { /* ... */ }
};

class Player : public GameObject {
public:
    Player(int id) : GameObject(id) {}
    void move(float dx, float dy) {
        // We use the protected setter instead of touching raw data
        setPosition(x + dx, y + dy); 
    }
};

By doing this, you've achieved the best of both worlds. The rest of the program is locked out, the subclasses have the power they need, and the base class retains control over how its data is changed. If you ever change how positions are stored internally, you only have to update the setPosition method in one place, and all your derived classes will keep working perfectly. It's a bit more typing upfront, but it saves you from a massive refactoring headache six months down the line.




📋 Practical Task

Refactoring the Vehicle Telemetry System

You have been handed a legacy piece of code for a vehicle tracking system. Currently, it uses a naive inheritance structure where all telemetry data is public, allowing any part of the system to accidentally modify a vehicle's fuel level or odometer reading.

Your goal: Refactor the following code to ensure that fuelLevel and mileage are encapsulated. They should be inaccessible to the rest of the program, but the Truck subclass should be able to update them using protected methods provided by the Vehicle base class.

#include <iostream>
#include <string>

class Vehicle {
public:
    std::string licensePlate;
    float fuelLevel;
    float mileage;

    Vehicle(std::string plate) : licensePlate(plate), fuelLevel(100.0f), mileage(0.0f) {}
};

class Truck : public Vehicle {
public:
    Truck(std::string plate) : Vehicle(plate) {}

    void drive(float distance, float fuelConsumed) {
        mileage += distance;
        fuelLevel -= fuelConsumed;
    }
};

int main() {
    Truck myTruck("ABC-123");
    myTruck.drive(50.0f, 10.0f);
    
    // This line represents a bug in the system where 
    // external code accidentally resets the fuel.
    // Your refactor should make this line cause a compiler error.
    myTruck.fuelLevel = 100.0f; 

    return 0;
}

Requirements:

  • Move fuelLevel and mileage to a private section in Vehicle.
  • Create protected setter methods in Vehicle that allow derived classes to update these values.
  • Update the Truck::drive method to use these setters instead of direct variable access.
  • Verify that the line myTruck.fuelLevel = 100.0f; in main no longer compiles.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.