Detailed Explanation of C++ Polymorphism (Part 2)

Last time we learned some basic concepts of polymorphism and the underlying implementation principles of dynamic binding. This time, let’s continue to explore the practical application scenarios of polymorphism.Before that, let’s supplement a few basic points that were missed last time.

Pure Virtual Functions and Abstract Classes

If a base class’s virtual function only needs to define an interface without an implementation, it can be defined as apure virtual function(by adding=0 after the function declaration). A class containing pure virtual functions is called anabstract class, and abstract classes cannot instantiate objects; they can only be inherited as base classes.

A pure virtual function forces derived classes to override that function, which is different from ordinary virtual functions. This characteristic reflects the nature of abstract classes, making them like an “interface template” that defines a series of interface forms, which are then specifically implemented by derived classes.

Let’s look at a specific example.

class Shape {  // Abstract classpublic:    virtual void draw() const = 0; // Pure virtual function: only defines the interface, no implementation    virtual ~Shape() {}  // Virtual destructor}; // Shape shape;  // Error: abstract class cannot instantiate objectsclass Circle : public Shape {public:    void draw() const override {  // Must override pure virtual function, otherwise Circle is also an abstract class        cout << "Drawing Circle" << endl;    }};class Rectangle : public Shape {public:    void draw() const override {  // Must override pure virtual function        cout << "Drawing Rectangle" << endl;    }};int main() {    Shape* ptr1 = new Circle();  // Correct: abstract class pointer can point to derived class object    ptr1->draw();  // Output: Drawing Circle    delete ptr1;    Shape* ptr2 = new Rectangle();      ptr2->draw();  // Output: Drawing Rectangle    delete ptr2;                return 0;}

Through this design, we can use a unified <span><span>Shape</span></span> pointer to operate on all shapes while ensuring that each shape implements the necessary functionality — this is the essence of C++ polymorphism: “unified interface, varied implementation”.

In the above example, we can also see that the destructor of the abstract class Shape is declared as a virtual function. Is this necessary?

The base class destructor must be a virtual function.

If the base class destructor is not a virtual function, when a base class pointer points to a derived class object and is deleted, only the base class destructor will be called, and the derived class destructor will not be called, leading tomemory leaks.

Let’s look at the following example.

class Base {public:    ~Base() {  // Non-virtual destructor        cout << "Base Destructor" << endl;    }};class Derived : public Base {private:    int* data;public:    Derived() { data = new int[10]; }  // Allocate memory    ~Derived() {  // Derived class destructor, release memory        delete[] data;        cout << "Derived Destructor (memory released)" << endl;    }};int main() {    Base* ptr = new Derived();  // Base class pointer points to derived class object    delete ptr;  // Only Base's destructor will be called, Derived's destructor will not be called    return 0;}

Running resultDetailed Explanation of C++ Polymorphism (Part 2)Only the base class destructor is called here, and it is clear that a memory leak has occurred.Change the Base class destructor tovirtual ~Base(), and the running result will beDetailed Explanation of C++ Polymorphism (Part 2)Then the derived class destructor will be called first, followed by the base class destructor, properly releasing memory.However, note that the following functions cannot be virtual functions:

  • Constructors: When the constructor is executed, the object’s vptr has not yet been initialized, so it cannot point to the correct vtable, thus it cannot be a virtual function.
  • Static member functions: Static member functions belong to the class, not to the object, and do not have a this pointer, so they cannot access vptr, thus cannot be virtual functions.
  • Inline functions: Inline functions are expanded at compile time, while virtual functions are called at runtime, which contradicts each other (the compiler will ignore the inline keyword and treat it as a normal virtual function).

Practical Application Scenarios of Polymorphism

Polymorphism is widely used in project development. Here are a few typical scenarios.

1. Simple Calculator Design: Define various operation methods through derived classes for flexible expansion.

class AbstractCalculator {public:    virtual int getResult() = 0;    int m_num1, m_num2;};class AddCalculator : public AbstractCalculator {public:    int getResult() override { return m_num1 + m_num2; }}; // New feature expansion (without modifying existing classes)class ExpCalculator : public AbstractCalculator {public:    int getResult() override { return pow(m_num1, m_num2); }}; // Usage AbstractCalculator* calc = new ExpCalculator();calc->m_num1 = 2;calc->m_num2 = 3;cout << calc->getResult();  // Output 8

2. Strategy Pattern: Dynamically switch algorithms.

The strategy pattern is a commonly used design pattern, and its core idea is to “encapsulate algorithms into independent classes and dynamically switch algorithms through polymorphism.” For example, in an e-commerce platform, there are different discount calculations for new users, members, and holidays, using different discount algorithms in different scenarios.

Code example: Strategy pattern implementation for discount calculation.

// Abstract strategy class: Discount algorithmclass DiscountStrategy{public:    // Calculate discount price    virtual double calculateDiscount(double price) const = 0;     virtual ~DiscountStrategy() {}};// Concrete strategy 1: New user discount (10% off)class NewUserDiscount : public DiscountStrategy{public:    double calculateDiscount(double price) const override    {        cout << "Using new user discount (10% off)" << endl;        return price * 0.9;    }};// Concrete strategy 2: Member discount (20% off)class MemberDiscount : public DiscountStrategy{public:    double calculateDiscount(double price) const override    {        cout << "Using member discount (20% off)" << endl;        return price * 0.8;    }};// Concrete strategy 3: Holiday discount (30% off)class FestivalDiscount : public DiscountStrategy{public:    double calculateDiscount(double price) const override    {        cout << "Using holiday discount (30% off)" << endl;        return price * 0.7;    }};// Context class: Order (using discount strategy)class Order{private:    double originalPrice;       // Original price    DiscountStrategy *strategy; // Discount strategy (base class pointer)public:    Order(double price, DiscountStrategy *s) : originalPrice(price), strategy(s) {}    // Calculate final price    double getFinalPrice() const    {        return strategy->calculateDiscount(originalPrice);    }    // Dynamically switch discount strategy    void setDiscountStrategy(DiscountStrategy *s)    {        delete strategy;        strategy = s;    }    ~Order()    {        delete strategy;    }};int main(){    // 1. New user places an order (original price 1000 yuan)    Order order(1000, new NewUserDiscount());    cout << "Final price: " << order.getFinalPrice() << " yuan" << endl;    cout << "------------------------" << endl;    // 2. Switch to member discount    order.setDiscountStrategy(new MemberDiscount());    cout << "Final price: " << order.getFinalPrice() << " yuan" << endl;    cout << "------------------------" << endl;    // 3. Switch to holiday discount    order.setDiscountStrategy(new FestivalDiscount());    cout << "Final price: " << order.getFinalPrice() << " yuan" << endl;    return 0;}

Through polymorphism, we can add new discount strategies (such as “Black Friday discount”) at any time without modifying the code of the Order class, which complies with the “Open-Closed Principle” (open for extension, closed for modification).

3. Callback Mechanism: Event Response and Notification.

In GUI development or network programming, it is often necessary to “automatically call the corresponding handling function when a certain event occurs”; this is the callback mechanism. Polymorphism is a common way to implement callbacks — the base class defines the callback interface (virtual function), and the derived class implements the specific callback logic.

For example, in Qt, the “button click event” automatically calls the handling function we define when the button is clicked, which is implemented through virtual functions at the bottom level.

4. Container Storage of Different Types of Objects.

C++ containers (such asvector and list) can only store data of the same type, but through polymorphism, we can use a “base class pointer container” to store objects of different derived classes, managing and operating them uniformly.

For example, in an “Animal Management System”, we can use vector<Animal*> to store objects of Dog, Cat, Bird, etc., and traverse to call the makeSound() function, where each animal will make a different sound.

There are many practical application scenarios for polymorphism, and many design patterns utilize polymorphism, which needs to be gradually experienced and mastered in future studies.

Disadvantages of Polymorphism

Polymorphism has many advantages, but using these advantages comes at a cost. Due to the underlying virtual function table and virtual function pointers, runtime requires a secondary mapping to look up the specific calling function. Additionally, virtual functions are almost impossible for the compiler to inline (GCC and MSVC explicitly prohibit this, and Clang may optimize only in simple scenarios), leading to increased overhead for calling small functions. However, generally speaking, the overhead of virtual table lookup is diluted by the time spent on business logic, making it negligible.

In modern CPU architecture, the overhead of calling virtual functions can almost be ignored, and in most scenarios, we do not need to consider it. However, we should be aware of this disadvantage. In some specific scenarios, targeted optimizations should be made. We will learn more about specific optimization techniques as our studies deepen.

Leave a Comment