Unit 4: Operator Overloading, Type Casting and Re-usability

CAP455 — Object Oriented Programming Using C++ 2 min read

I. Orientation — Extending and Reusing C++ Types

C++ supports user-defined types that can behave like built-in types and can reuse existing class implementations. Operator overloading gives operators a class-specific meaning, type conversion connects class objects with other types, and inheritance creates new classes from established ones.

  • Governing principle: Existing C++ syntax and tested class functionality should be extended without changing their fundamental meaning.
  • Core mechanisms:
    • Operator overloading: Defines operator behavior for class or enumeration operands.
    • Type conversion: Transforms a value from a basic type to a class type or vice versa.
    • Inheritance: Creates a derived class by acquiring and extending a base class.
  • Important conventions:
    • At least one operand of an overloaded operator must have a class or enumeration type.
    • Operator precedence, associativity, and number of operands cannot be changed.
    • Constructors initialize objects; destructors release resources.
    • Access depends on both the member’s access specifier and the inheritance mode.

II. Operator Overloading — Natural Operations on Objects

Operator overloading assigns a class-specific implementation to an existing C++ operator through a specially named function such as operator+.

A. Importance of operator overloading

Operator overloading makes expressions involving objects concise and consistent with expressions involving built-in values.

  • Readability: c1 + c2 is clearer than c1.add(c2) for complex numbers.
  • Abstraction: The caller uses an operator without knowing its internal implementation.
  • Consistency: Operators such as +, ==, [], and << can express conventional class operations.
  • Restrictions:
    • New operators cannot be created.
    • Operators such as ::, ., .*, ?:, and sizeof cannot be overloaded.
    • Overloading cannot change + from a binary operator into a ternary operator.
  • Good practice: Preserve expected meaning; + should normally create a combined value rather than unexpectedly modify both operands.

B. Unary operator overloading

A unary overloaded operator acts on one object, as in negation, increment, or decrement.

  • Member form: A unary member operator takes no explicit operand because *this is its operand.
  • Prefix form: operator++() performs increment before the resulting value is used.
  • Postfix form: operator++(int) uses an unused int parameter to distinguish postfix syntax.
  • Example:
CPP
class Counter {
    int value;
public:
    Counter(int v = 0) : value(v) {}

    Counter& operator++() {       // prefix ++c
        ++value;
        return *this;
    }

    Counter operator-() const {   // unary -c
        return Counter(-value);
    }
};
  • Return choice: Prefix increment commonly returns Counter&, while unary negation returns a new Counter.

C. Binary operator overloading

A binary overloaded operator combines or compares two operands, such as a + b or a == b.

  • Member form: A binary member operator receives one explicit argument; its left operand is *this.
  • Non-member form: A non-member operator receives both operands and is useful for symmetric conversions.
  • Example:
CPP
class Complex {
    double real, imag;
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}

    Complex operator+(const Complex& rhs) const {
        return Complex(real + rhs.real, imag + rhs.imag);
    }
};
  • Expression: For Complex c3 = c1 + c2;, the compiler interprets the member form as c1.operator+(c2).
  • Const correctness: const Complex& avoids copying rhs, and the final const guarantees that the left operand is unchanged.

III. Type Casting — Conversions Involving Class Objects

Class-related conversions are commonly implemented through converting constructors or conversion functions, subject to overload resolution and access control.

A. Basic type to class type conversion

A single-argument constructor can convert a basic value into an object of its class.

  • Mechanism: Distance(double) can construct a Distance object from a double.
  • Example:
CPP
class Distance {
    double metres;
public:
    Distance(double m) : metres(m) {}
};

Distance d = 12.5;   // double converted to Distance
  • Conversion sequence: The compiler evaluates 12.5, calls Distance(12.5), and initializes d.
  • Control with explicit: Declaring explicit Distance(double m) prevents implicit initialization such as Distance d = 12.5; Distance d(12.5) remains valid.
  • Design rule: Use implicit conversion only when the basic value has a clear, unambiguous class interpretation.

B. Class type to basic type conversion

A conversion function transforms an object into a basic type and has the form operator type().

  • Syntax: It has no declared return type because the target type follows the operator keyword.
  • Example:
CPP
class Distance {
    double metres;
public:
    Distance(double m) : metres(m) {}
    explicit operator double() const {
        return metres;
    }
};

Distance d(8.4);
double m = static_cast<double>(d);
  • Meaning: operator double() returns the stored value 8.4 as a double.
  • Safety: explicit prevents unintended conversions in arithmetic or function calls.
  • Const qualification: The final const permits conversion of constant objects and promises not to modify the source object.

IV. Inheritance Foundations — Reuse and Class Relationships

Inheritance defines a new class in terms of an existing class, representing an “is-a” relationship when used appropriately.

A. Importance of re-usability through inheritance

Inheritance promotes reuse by placing common state and behavior in a base class.

  • Reduced duplication: Car and Truck can inherit shared operations such as start() from Vehicle.
  • Extensibility: A derived class can add data or redefine behavior without rewriting the base implementation.
  • Maintenance: A correction in an inherited base function becomes available to derived classes.
  • Limitation: Inheritance creates coupling; composition is preferable for a “has-a” relationship such as Car having an Engine.

B. Basics of inheritance

Inheritance syntax names a base class and an access mode in the derived-class declaration.

  • General form:
CPP
class Derived : public Base {
    // additional members
};
  • Acquisition: A derived object contains a base-class subobject.
  • Visibility: Private base members exist in that subobject but cannot be accessed directly by derived member functions.
  • Non-inherited operations: Constructors, destructors, and assignment operators are not inherited in the ordinary sense, although constructors can be exposed with using Base::Base.

C. Base class and derived class

The base class supplies reusable features, while the derived class specializes or extends them.

  • Base class: Defines general members, such as Vehicle::move().
  • Derived class: Adds specialized members, such as Car::openBoot().
  • Access: Protected members are directly usable in derived member functions; private members require public or protected base functions.
  • Substitution: Under public inheritance, a Derived* or Derived& can be used where an accessible Base* or Base& is expected.

V. Forms of Inheritance — Class-Relationship Structures

Inheritance forms are classified by the number and arrangement of base and derived classes.

A. Simple inheritance

Simple inheritance has one derived class and one direct base class.

  • Structure: class SavingsAccount : public Account.
  • Purpose: The derived class reuses Account features and adds savings-specific behavior.
  • Shape: Account → SavingsAccount.

B. Multilevel inheritance

Multilevel inheritance forms a chain in which one derived class becomes another class’s base.

  • Structure: Person → Employee → Manager.
  • Effect: Manager contains both an Employee base subobject and, within it, a Person base subobject.
  • Access condition: Features remain accessible only if each inheritance level permits access.

C. Multiple inheritance

Multiple inheritance gives one derived class two or more direct base classes.

  • Structure:
CPP
class SmartPhone : public Camera, public Phone {};
  • Benefit: SmartPhone combines camera and telephone interfaces.
  • Risk: Identically named members or a repeated common ancestor can create ambiguity.
  • Construction: Direct bases are initialized in their declaration order, here Camera before Phone.

D. Hierarchical inheritance

Hierarchical inheritance derives multiple classes from one common base class.

  • Structure: Shape → Circle, Shape → Rectangle.
  • Reuse: Both derived classes can share Shape::colour.
  • Specialization: Each class can provide its own implementation of an operation such as area().

E. Hybrid inheritance

Hybrid inheritance combines two or more forms, often hierarchical and multiple inheritance.

  • Diamond structure: Student and Employee derive from Person, while TeachingAssistant derives from both.
  • Problem: Without virtual inheritance, TeachingAssistant contains two Person subobjects.
  • Solution: Declare Person as a virtual base when only one shared Person subobject is intended.

VI. Inheritance Modes — Controlling Acquired Access

The inheritance mode determines how accessible base-class members are treated inside and through the derived class.

A. Private mode of inheritance

Private inheritance makes inherited public and protected members private within the derived class.

  • Mapping: Base public → private; base protected → private.
  • External access: Users cannot implicitly treat the derived object as a publicly accessible base object.
  • Meaning: It usually models “implemented in terms of” rather than a public “is-a” relationship.
  • Default: Inheritance is private when a class declaration omits the mode.

B. Protected mode of inheritance

Protected inheritance converts inherited public and protected members into protected members.

  • Mapping: Base public → protected; base protected → protected.
  • Availability: The derived class and its descendants can use those members.
  • Restriction: Ordinary external code cannot access the inherited interface through the derived object.
  • Use: It supports controlled reuse across an inheritance hierarchy without exposing the base interface publicly.

C. Public mode of inheritance

Public inheritance preserves the accessible status of base public and protected members.

  • Mapping: Base public → public; base protected → protected.
  • Private members: Base-private data remains inaccessible directly in the derived class under every mode.
  • Relationship: Public inheritance normally represents “Derived is a Base.”
  • Polymorphic use: A derived object can be referenced through an accessible base pointer or reference.

VII. Inherited Behavior and Object Lifetime

Derived classes can replace inherited behavior, while construction and destruction follow a fixed hierarchy-safe sequence.

A. Overriding of member functions

Overriding occurs when a derived class provides a function matching a virtual base-class function.

  • Requirement: The parameter list and relevant qualifiers must match; a covariant return type is allowed for pointers or references to related classes.
  • Example:
CPP
class Shape {
public:
    virtual void draw() const {}
    virtual ~Shape() = default;
};

class Circle : public Shape {
public:
    void draw() const override {}
};
  • Dynamic dispatch: Calling draw() through a Shape& bound to a Circle invokes Circle::draw().
  • override specifier: It causes a compile-time error if the function does not actually override.
  • Virtual destructor: It ensures that deleting a derived object through a base pointer runs both destructors correctly.

B. Order of execution of constructors and destructor

Construction proceeds from base parts to derived parts, while destruction reverses that order.

  • Construction order:
    1. Virtual base classes.
    2. Direct base classes in declaration order.
    3. Data members in declaration order.
    4. Derived constructor body.
  • Destruction order: The derived destructor body runs first, followed by members and direct bases in reverse order; virtual bases are destroyed last.
  • Reason: A derived constructor may safely rely on a fully initialized base subobject.
  • Important detail: Initializer-list order does not override declaration order.

VIII. Ambiguity and Shared Bases — Managing Complex Hierarchies

Complex inheritance may introduce duplicate names or duplicate base subobjects, requiring explicit qualification or virtual inheritance.

A. Resolving ambiguities in inheritance

Ambiguity occurs when the compiler finds multiple equally valid inherited members.

  • Qualification: If both Printer and Scanner define start(), use obj.Printer::start() or obj.Scanner::start().
  • Derived wrapper: The derived class can define its own start() and explicitly select or combine base implementations.
  • Declaration selection: A using Base::function; declaration can expose a chosen overloaded base function.
  • Diamond ambiguity: A non-virtual diamond contains two common-base subobjects, making an unqualified common-base member ambiguous.

B. Virtual base class

A virtual base class ensures that the most-derived object contains only one shared subobject of a repeated base.

  • Declaration:
CPP
class Person {};
class Student : virtual public Person {};
class Employee : virtual public Person {};
class Assistant : public Student, public Employee {};
  • Object structure: Assistant contains one shared Person, not separate Person parts through Student and Employee.
  • Initialization responsibility: The most-derived class, Assistant, directly initializes the virtual base.
  • Benefit: It resolves duplicated state and common-base conversion ambiguity in diamond inheritance.
  • Cost: Virtual inheritance adds implementation complexity and may require extra runtime layout information.