Object-Oriented Programming with C++

Chapter 5
Relationships, Inheritance, and Polymorphism

Bachelor Program in Mechatronics Engineering
Object-Oriented Programming with C++   •   Bachelor

Relationships, Inheritance, and Polymorphism

Introduction

Object-oriented programming (OOP) becomes particularly powerful when software must represent systems containing interacting entities. A mechatronics application may model motors, sensors, controllers, robotic joints, safety devices, and communication modules. These entities are not isolated objects: a robot contains joints, a controller communicates with sensors, and several actuator types may share a common interface. C++ provides language mechanisms for expressing these relationships, including association, aggregation, composition, inheritance, abstract classes, virtual functions, overriding, dynamic binding, interfaces, and multiple inheritance.

The central design question is not simply whether inheritance is available. It is whether a relationship should be represented by inheritance or by object composition. Inheritance expresses a type relationship—“is-a”—whereas composition expresses a structural or ownership relationship—“has-a.” A reliable design uses the least complicated relationship that accurately represents the domain.

Learning Objectives

By the end of this chapter, you should be able to:

  • distinguish association, aggregation, and composition;
  • implement inheritance and explain public, protected, and private inheritance;
  • create abstract classes and interface-like contracts;
  • use virtual functions, overriding, and dynamic binding safely;
  • explain object lifetime and destructor requirements in polymorphic hierarchies;
  • apply multiple inheritance carefully;
  • use dynamic_cast, typeid, and std::unique_ptr appropriately;
  • decide when composition is preferable to inheritance;
  • design a small extensible mechatronics software system.

1. Object Relationships

1.1 Association

An association is a general relationship in which one object knows about or uses another. Neither object necessarily owns the other. For example, a Controller may refer to a TemperatureSensor supplied by another subsystem.

class TemperatureSensor {
public:
    double readCelsius() const;
};

class Controller {
    TemperatureSensor* sensor;       // non-owning association
public:
    explicit Controller(TemperatureSensor* s) : sensor{s} {}
    double temperature() const { return sensor->readCelsius(); }
};

The pointer does not by itself indicate ownership. The sensor may be created, managed, and destroyed elsewhere. In modern C++, a reference or pointer should communicate this design clearly. A reference is suitable when the association must always refer to an object; a pointer may represent optionality or reseating.

1.2 Aggregation

Aggregation is a weak whole–part relationship. The aggregate refers to parts, but the parts can exist independently and may be shared. A laboratory system may aggregate several sensors that are registered with it while the sensors remain owned by a separate device manager.

class Sensor;

class SensorNetwork {
    std::vector<Sensor*> sensors;    // non-owning aggregation
public:
    void add(Sensor& s) { sensors.push_back(&s); }
};

Aggregation is primarily a conceptual modeling term; C++ does not have a special aggregation keyword. The implementation must establish ownership and lifetime rules explicitly.

1.3 Composition

Composition is a strong whole–part relationship. The part is owned by the whole, and its lifetime is normally bounded by the whole. A robotic joint may contain a position controller that is created and destroyed with the joint.

class PositionController {
public:
    void update(double target, double actual);
};

class RoboticJoint {
    PositionController controller;    // composed subobject
public:
    void moveTo(double target, double actual) {
        controller.update(target, actual);
    }
};

Composition may also use smart pointers when construction is conditional or when polymorphic parts are required:

class Joint {
    std::unique_ptr<PositionController> controller;
public:
    Joint() : controller{std::make_unique<PositionController>()} {}
};

std::unique_ptr expresses exclusive ownership and automatically releases the object. std::shared_ptr should be reserved for genuine shared ownership because reference counting adds complexity and can hide lifetime errors.

1.4 Comparison of Relationships

RelationshipMeaningTypical lifetimeCommon C++ representation
AssociationUses or knowsIndependentReference, pointer, observer handle
AggregationWeak whole–partPart survives wholeContainer of references or non-owning pointers
CompositionStrong whole–partPart tied to wholeData member or owning smart pointer
InheritanceType specializationBase-subobject within derived objectclass D : public B

A relationship diagram or class model should specify whether a reference is owning, non-owning, optional, shared, or exclusive. Ambiguous ownership is a major source of memory and resource defects.

2. Inheritance as a Type Relationship

Inheritance enables a derived class to reuse and specialize a base class. Public inheritance should model substitutability: an object of the derived type should be usable wherever an object of the base type is expected. This is commonly described as the Liskov substitution principle.

class Sensor {
public:
    void enable() { enabled = true; }
    bool isEnabled() const { return enabled; }
protected:
    bool enabled{false};
};

class PressureSensor : public Sensor {
public:
    double pressureKPa() const;
};

A PressureSensor is a Sensor, so public inheritance may be appropriate. The derived class contains a base-class subobject. Constructors initialize the base before the derived members; destruction occurs in the reverse order.

2.1 Access and Inheritance Modes

The inheritance mode affects how inherited public and protected members are exposed through the derived type.

ModeBase public members through derived objectBase protected membersUsual meaning
publicPublicProtectedPublic subtype relationship
protectedProtectedProtectedImplementation-oriented reuse
privatePrivatePrivate“Implemented in terms of,” rarely a subtype

Private data should normally remain private. A derived class should use a protected or public function rather than directly modifying an invariant-critical data member.

2.2 Construction, Copying, and Destruction

A derived constructor must initialize the base appropriately:

class Device {
    int id;
public:
    explicit Device(int deviceId) : id{deviceId} {}
    int identifier() const { return id; }
};

class Motor : public Device {
    double ratedVoltage;
public:
    Motor(int id, double voltage)
        : Device{id}, ratedVoltage{voltage} {}
};

If a base class owns resources, its special member functions must follow the rule of zero, rule of five, or an explicitly justified design. In most application code, standard library resource managers should be used rather than manually managing memory.

3. Virtual Functions and Runtime Polymorphism

Polymorphism allows one interface to represent multiple implementations. A virtual function permits a call through a base pointer or reference to select the overriding function according to the object’s runtime type. This is dynamic binding.

class Actuator {
public:
    virtual void setCommand(double value) = 0;
    virtual double feedback() const = 0;
    virtual ~Actuator() = default;
};

class MotorActuator : public Actuator {
    double command{0.0};
public:
    void setCommand(double value) override { command = value; }
    double feedback() const override { return command; }
};

class ValveActuator : public Actuator {
    double opening{0.0};
public:
    void setCommand(double value) override { opening = value; }
    double feedback() const override { return opening; }
};

The expressions = 0 make the functions pure virtual. A class containing at least one pure virtual function is abstract and cannot be instantiated directly. It defines a contract for derived classes.

void command(Actuator& a, double value) {
    a.setCommand(value);       // dynamically dispatched
}

If a refers to a MotorActuator, the motor implementation is called; if it refers to a ValveActuator, the valve implementation is called. The caller depends only on the stable Actuator interface.

3.1 Overriding and Overloading

Overriding replaces a virtual function in a derived class with the same signature. The override specifier instructs the compiler to verify this intention.

class Motor : public Actuator {
public:
    void setCommand(double value) override; // overriding
};

Overloading, by contrast, means using the same function name with different parameter lists, generally in the same scope. It does not necessarily involve inheritance or dynamic dispatch. The override keyword is therefore an important error-prevention tool.

A function declared final cannot be overridden further:

class SafetyActuator : public Actuator {
public:
    void setCommand(double value) final;
};

3.2 Virtual Destructors

If a class is intended to be used polymorphically, its destructor should normally be virtual. Otherwise, deleting a derived object through a base pointer produces undefined behavior.

Actuator* p = new MotorActuator;
delete p;   // safe because Actuator::~Actuator is virtual

Prefer automatic lifetime management:

std::unique_ptr<Actuator> p = std::make_unique<MotorActuator>();

The smart pointer still requires a virtual base destructor for correct destruction through the base type.

3.3 Object Slicing

Passing a derived object by value to a function expecting a base object copies only the base subobject. The derived portion is sliced away.

void logDevice(Device d);       // slicing can occur
void logDevice(const Device& d); // preserves polymorphic identity

Use references or pointers for polymorphic parameters. Pass by value only when slicing is intended or when the type is not used polymorphically.

4. Interfaces and Abstract Design

C++ has no separate interface keyword. An interface is commonly represented by an abstract class containing pure virtual functions, a virtual destructor, and little or no data.

class IPositionSource {
public:
    virtual double positionMillimetres() const = 0;
    virtual ~IPositionSource() = default;
};

The leading I is a naming convention, not a language requirement. An interface supports dependency inversion: high-level control logic depends on an abstraction rather than on a particular sensor model.

class ControlLoop {
    IPositionSource& source;
public:
    explicit ControlLoop(IPositionSource& s) : source{s} {}
    double measurement() const { return source.positionMillimetres(); }
};

This design improves testing because a simulated source can implement the same contract. It also allows hardware replacement without rewriting control logic.

An abstract class may contain implemented functions and state, whereas a pure interface is generally stateless and contract-focused. The distinction is architectural rather than enforced completely by C++.

5. Multiple Inheritance

Multiple inheritance allows a class to have more than one direct base class. It is often useful when combining independent interfaces.

class IReadable {
public:
    virtual double read() const = 0;
    virtual ~IReadable() = default;
};

class ICalibratable {
public:
    virtual void calibrate() = 0;
    virtual ~ICalibratable() = default;
};

class SmartSensor : public IReadable, public ICalibratable {
public:
    double read() const override { return 0.0; }
    void calibrate() override {}
};

This form is comparatively safe because the bases represent separate capabilities. Problems arise when bases contain overlapping state or the same member names. The classic diamond occurs when two intermediate classes inherit from a common base and a final class inherits from both. Without virtual inheritance, the final object contains two base subobjects.

class Device { protected: int id{0}; };
class InputDevice : virtual public Device {};
class OutputDevice : virtual public Device {};
class IODDevice : public InputDevice, public OutputDevice {};

Virtual inheritance ensures one shared Device subobject, but it complicates construction and layout. Prefer composition or independent interfaces unless multiple inheritance clearly models the design.

6. Safe Runtime Type Use

Polymorphism should normally be achieved through virtual functions, not by repeatedly inspecting concrete types. Nevertheless, safe runtime type operations are sometimes necessary—for example, an optional capability or a diagnostic tool.

dynamic_cast checks a polymorphic hierarchy at runtime:

void calibrateIfSupported(Actuator& actuator) {
    if (auto* c = dynamic_cast<Calibratable*>(&actuator)) {
        c->calibrate();
    }
}

A pointer cast returns nullptr on failure. A reference cast throws std::bad_cast on failure:

try {
    auto& c = dynamic_cast<Calibratable&>(actuator);
    c.calibrate();
} catch (const std::bad_cast&) {
    // capability is not available
}

The source type must be polymorphic, normally by having at least one virtual function. dynamic_cast is safer than a C-style cast because it checks the relationship. However, frequent downcasting may indicate that the base interface is incomplete or that the design should use capability interfaces.

typeid can report a runtime type, but direct comparisons to concrete types tend to create brittle code. Use it mainly for diagnostics, logging, or carefully justified framework code.

Never use static_cast to downcast unless the programmer can prove the object has the target type. If that assumption is wrong, behavior is undefined. Never reinterpret unrelated object types to force a relationship.

7. Composition or Inheritance?

The following decision framework is useful:

  1. Is the proposed derived object genuinely substitutable for the base object?
  2. Does the base contract describe behavior rather than accidental implementation detail?
  3. Must clients operate on a family of interchangeable types?
  4. Would composition permit the behavior to change at runtime?
  5. Is inheritance being used only to reuse code?

If the answer to the first three questions is no, composition is usually preferable. For example, a Robot should not inherit from Motor merely because it uses a motor. A robot has a motor. Conversely, a BrushlessMotor may inherit from an abstract Motor interface if all motors satisfy the same contract.

Composition offers lower coupling, clearer ownership, easier testing, and the ability to replace components. Inheritance can offer substitutability and a coherent polymorphic family, but it creates stronger compile-time coupling and makes base-class changes potentially affect derived classes.

8. Detailed Applied Case: Modular Robotic Drive Controller

The following is an illustrative case, not a report of a particular commercial product.

Background

A mobile robot requires a controller that can command either a DC motor drive or a servo drive. The controller must also accept simulated actuators during software testing. Each drive reports feedback and may support emergency stopping.

Problem

An initial design uses a large conditional statement:

if (type == DriveType::DC) {
    // DC-specific behavior
} else if (type == DriveType::Servo) {
    // servo-specific behavior
}

This approach becomes difficult to extend and encourages the controller to know hardware details. It also makes simulation and safety testing harder.

Analysis

The controller requires a stable abstraction with operations common to all drives. A drive is an appropriate polymorphic family because every supported drive can receive a command and report feedback. Emergency stop is a separate capability rather than necessarily a property of every possible actuator, so it can be represented by another interface.

Approach and Solution

#include <iostream>
#include <memory>
#include <vector>

class Drive {
public:
    virtual void command(double normalized) = 0;
    virtual double feedback() const = 0;
    virtual ~Drive() = default;
};

class EmergencyStop {
public:
    virtual void stopImmediately() = 0;
    virtual ~EmergencyStop() = default;
};

class ServoDrive final : public Drive, public EmergencyStop {
    double position{0.0};
    bool stopped{false};
public:
    void command(double normalized) override {
        if (!stopped) position = normalized;
    }
    double feedback() const override { return position; }
    void stopImmediately() override { stopped = true; }
};

class DriveController {
    std::vector<std::unique_ptr<Drive>> drives;
public:
    void addDrive(std::unique_ptr<Drive> drive) {
        drives.push_back(std::move(drive));
    }
    void commandAll(double value) {
        for (auto& drive : drives) drive->command(value);
    }
};

DriveController owns its drives through std::unique_ptr, which expresses composition and prevents leaks. The vector stores different concrete drive types through the common Drive interface. Dynamic binding selects the correct implementation at runtime. ServoDrive uses multiple inheritance to express two independent capabilities.

If emergency stopping must be available for every drive, it should instead be part of the main Drive interface. If only some devices support it, a separate capability interface and a carefully justified dynamic_cast are appropriate.

Outcome

A simulated drive can be added without changing DriveController. Hardware-specific code is isolated in concrete classes. Ownership is explicit, destruction is safe, and extension is localized.

Learning Points

  • use inheritance for a genuine substitutable type family;
  • use composition to represent ownership and system structure;
  • use interfaces to separate capabilities;
  • use smart pointers for resource ownership;
  • prefer virtual dispatch over type-code conditionals;
  • treat downcasting as an exception, not the primary design mechanism.

9. Limitations, Risks, and Best Practices

Common technical risks

Non-virtual destructor: can cause undefined behavior when deleting through a base pointer.

Missing override: a signature mismatch may silently create a new function instead of overriding the intended one.

Object slicing: destroys derived behavior when polymorphic objects are copied by value.

Dangling associations: non-owning pointers outlive their targets.

Ownership confusion: raw pointers do not document who must delete an object.

Fragile base classes: changes to protected implementation details can break derived classes.

Excessive inheritance: deep hierarchies are difficult to understand, test, and modify.

Unsafe casts: unchecked downcasts and C-style casts can produce undefined behavior.

Engineering and professional considerations

In safety-relevant mechatronics systems, polymorphism must not compromise determinism, traceability, or verification. Dynamic allocation may be restricted in hard real-time or certified code. A project may therefore use fixed-capacity containers, static ownership, and explicitly bounded execution. The design should follow the applicable safety standard and organizational coding rules; C++ language mechanisms alone do not establish functional safety.

Interfaces should be small and stable. Constructors should establish valid invariants, and public functions should document units, ranges, error behavior, and thread-safety expectations. For example, a command described as “normalized” should specify whether its valid range is [−1, 1], [0, 1], or another interval. Tests should cover base-interface behavior, concrete implementations, ownership, destruction, failed casts, and boundary conditions.

Modern C++ best practice generally includes:

  • prefer the rule of zero;
  • use std::unique_ptr for exclusive ownership;
  • use std::shared_ptr only for genuine shared ownership;
  • mark overrides with override;
  • mark non-overridable functions or classes with final when useful;
  • use const and references to express intent;
  • keep interfaces minimal;
  • prefer composition unless substitutability is clear;
  • compile with strong warnings and use sanitizers during testing;
  • document lifetime and thread-safety contracts.

Contemporary Relevance

Robotics and industrial automation increasingly combine physical hardware with simulation, digital twins, plug-in drivers, and test doubles. Interface-based polymorphism supports these substitutions. Generic programming and concepts may replace some inheritance-based designs when behavior can be expressed at compile time, improving performance and diagnostics. Conversely, runtime polymorphism remains valuable when hardware types are selected from configuration files or loaded through stable software boundaries.

Artificial intelligence components can also be integrated through interfaces—for example, a perception module may expose a common detection contract while implementations use different models. The same governance principles apply: the interface should define data validity, timing, failure modes, and provenance. Abstraction does not remove the need to verify safety-critical outputs.

Chapter Summary

Association represents use or knowledge between independent objects. Aggregation represents a weak whole–part relationship in which parts can survive independently. Composition represents ownership and usually binds part lifetime to the whole. Inheritance expresses a type relationship and should be used only when a derived object is genuinely substitutable for its base.

Virtual functions enable runtime polymorphism. Pure virtual functions define abstract contracts, while override provides compile-time checking. Polymorphic bases should have virtual destructors, and polymorphic objects should be passed by reference or pointer to avoid slicing. C++ interfaces are commonly implemented as abstract classes with pure virtual functions. Multiple inheritance is most defensible when combining independent interfaces, although it can create ambiguity and diamond problems. dynamic_cast provides checked runtime type use, but frequent downcasting usually signals a design weakness. In most system structures, composition gives clearer ownership and lower coupling than inheritance.

For mechatronics software, these mechanisms support replaceable hardware drivers, simulation, testing, modular control systems, and capability-based design. Safe implementation requires explicit lifetime rules, disciplined interfaces, deterministic behavior where required, and compliance with applicable engineering and safety standards.

Key Terms

Association: A relationship in which one object uses or knows another without necessarily owning it.

Aggregation: A weak whole–part relationship whose parts can exist independently.

Composition: A strong ownership relationship in which a whole controls the lifetime of its parts.

Inheritance: A mechanism by which a derived class extends or specializes a base class.

Base class: The class from which another class inherits.

Derived class: A class that inherits from a base class.

Substitutability: The ability to use a derived object wherever its base type is expected.

Virtual function: A member function selected through dynamic dispatch when called via a base interface.

Pure virtual function: A virtual function declared with = 0.

Abstract class: A class that cannot be instantiated, usually because it has a pure virtual function.

Overriding: Providing a derived implementation of a virtual base function.

Overloading: Defining functions with the same name but different parameter lists.

Dynamic binding: Runtime selection of an overriding function.

Interface: An abstract behavioral contract exposed to clients.

Object slicing: Loss of the derived portion when a derived object is copied into a base object by value.

Multiple inheritance: Inheritance from more than one direct base class.

dynamic_cast: A checked runtime cast within a polymorphic hierarchy.

Smart pointer: A class that manages object lifetime according to ownership semantics.

Composition over inheritance: A design preference for assembling behavior from owned components when subtype inheritance is not justified.

Review Questions

Short Answer Questions

  1. Distinguish association, aggregation, and composition.
  2. Why should a polymorphic base class usually have a virtual destructor?
  3. What is the difference between overriding and overloading?
  4. What problem does the override specifier help detect?
  5. Why can passing a derived object by value cause object slicing?

Long Answer Questions

  1. Explain how virtual functions, pure virtual functions, abstract classes, and dynamic binding work together in a C++ design.
  2. Compare composition and public inheritance, including their effects on coupling, ownership, testing, and extensibility.
  3. Describe the diamond problem in multiple inheritance and explain how virtual inheritance addresses it.
  4. Design an interface hierarchy for several types of robotic sensors and justify your choices of inheritance and composition.
  5. Explain safe and unsafe uses of dynamic_cast, static_cast, and typeid in a polymorphic system.

Analytical / Application Questions

  1. A robot controller owns several interchangeable actuator drivers, but some drivers support calibration and others do not. Propose classes and ownership relationships that model this system safely.
  2. A proposed design makes Robot inherit from Motor, Battery, and Sensor. Analyze the design and redesign it using appropriate relationships.
  3. A program stores derived sensor objects in std::vector<Sensor> and later observes that specialized behavior is lost. Explain the cause and provide two safer design alternatives.

Multiple Choice Questions with Answers

Question 1

Which relationship most clearly represents a robot owning a controller whose lifetime ends with the robot?

A. Association

B. Aggregation

C. Composition

D. Unrelated dependency

Correct Answer: C. Composition

Question 2

What is the primary purpose of a pure virtual function?

A. To prevent all inheritance

B. To define a contract that derived classes must implement

C. To allocate an object dynamically

D. To overload a constructor

Correct Answer: B. To define a contract that derived classes must implement

Question 3

Which declaration safely indicates that a derived function is intended to override a base virtual function?

A. void f() overload;

B. void f() virtual;

C. void f() override;

D. void f() final overrideable;

Correct Answer: C. void f() override;

Question 4

Why should a polymorphic base class normally have a virtual destructor?

A. To allow private inheritance

B. To ensure correct destruction through a base pointer

C. To make all functions pure virtual

D. To prevent dynamic binding

Correct Answer: B. To ensure correct destruction through a base pointer

Question 5

What happens when a derived object is passed by value to a function accepting a base object?

A. The derived object is automatically serialized

B. The derived part may be sliced away

C. The base class becomes abstract

D. Dynamic binding is strengthened

Correct Answer: B. The derived part may be sliced away

Question 6

What does a failed pointer dynamic_cast return?

A. false

B. -1

C. nullptr

D. An uninitialized pointer

Correct Answer: C. nullptr

Question 7

Which design most directly expresses exclusive ownership of a polymorphic object?

A. A raw pointer with no ownership documentation

B. std::unique_ptr

C. A global variable

D. A reference to a temporary object

Correct Answer: B. std::unique_ptr

Question 8

Which statement best describes public inheritance?

A. It normally models an “is-a” relationship and substitutability

B. It always means shared ownership

C. It prevents virtual dispatch

D. It is identical to composition

Correct Answer: A. It normally models an “is-a” relationship and substitutability

Question 9

When is multiple inheritance generally most defensible?

A. When combining small, independent capability interfaces

B. When avoiding all constructors

C. When replacing every data member

D. When forcing unrelated classes into one hierarchy

Correct Answer: A. When combining small, independent capability interfaces

Question 10

Which design choice usually reduces coupling when a class merely uses a motor rather than being a kind of motor?

A. Private data inheritance

B. Composition

C. Unchecked downcasting

D. Object slicing

Correct Answer: B. Composition

Object-Oriented Programming with C++
Chapter 5 of 14