Fundamentals 20 min read

How C++ Polymorphism Cuts Tight Coupling and Boosts Code Reuse

This article explains C++ polymorphism, shows how virtual functions and inheritance break tight coupling, improve code reuse, simplify extensions, and enhance maintainability, and demonstrates its role in common design patterns such as Strategy and Factory Method with clear code examples.

Deepin Linux
Deepin Linux
Deepin Linux
How C++ Polymorphism Cuts Tight Coupling and Boosts Code Reuse

In the world of C++ programming, polymorphism is a crucial feature that frequently appears in interview questions, such as Tencent's first‑round interview which focuses on how C++ polymorphism solves tricky programming problems.

As projects grow, code often becomes tightly coupled—like a set of gears where a small change affects the whole system. For example, a graphics‑drawing system may require many modifications across classes when adding a new shape, leading to high maintenance cost.

Polymorphism acts as a sharp blade that cuts through this tight coupling. By using virtual functions and inheritance, different objects can respond to the same message with varied behavior, providing a unified interface and diverse implementations.

1. Getting Started with Polymorphism

Just as the same action (driving) feels different for a race‑car driver versus a novice, C++ polymorphism allows the same operation to exhibit different behaviors. It is achieved mainly through virtual functions: a base‑class pointer or reference pointing to derived objects invokes the overridden function, e.g., an

Animal

class with a virtual

makeSound()

method overridden by

Dog

and

Cat

.

Polymorphism can be divided into static (compile‑time) and dynamic (run‑time). Static polymorphism uses function overloading and templates, while dynamic polymorphism relies on virtual functions.

2. Polymorphism Solves Code‑Reuse Problems

Without polymorphism, drawing different shapes requires separate functions:

<code>class Circle {
public:
    void drawCircle() {
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Rectangle {
public:
    void drawRectangle() {
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

class Triangle {
public:
    void drawTriangle() {
        std::cout << "Drawing a triangle" << std::endl;
    }
};

int main() {
    Circle circle;
    Rectangle rectangle;
    Triangle triangle;
    circle.drawCircle();
    rectangle.drawRectangle();
    triangle.drawTriangle();
    return 0;
}
</code>

Adding a new shape would require new functions and changes in calling code. By introducing a base

Shape

class with a pure virtual

draw()

method, all shapes inherit and override it:

<code>class Shape {
public:
    virtual void draw() = 0; // pure virtual
};

class Circle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a circle" << std::endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a rectangle" << std::endl;
    }
};

class Triangle : public Shape {
public:
    void draw() override {
        std::cout << "Drawing a triangle" << std::endl;
    }
};

void drawShapes(Shape* shapes[], int count) {
    for (int i = 0; i < count; ++i) {
        shapes[i]->draw();
    }
}

int main() {
    Circle circle;
    Rectangle rectangle;
    Triangle triangle;
    Shape* shapes[] = { &circle, &rectangle, &triangle };
    int count = sizeof(shapes) / sizeof(shapes[0]);
    drawShapes(shapes, count);
    return 0;
}
</code>

The

drawShapes

function now works for any new shape without modification, greatly improving reuse and extensibility.

3. Polymorphism Makes Extension Easy

In a role‑playing game, different character types (warrior, mage, assassin) each have unique attack and movement methods. Without polymorphism, adding a new character type forces many

if‑else

branches.

<code>class Warrior {
public:
    void attackWarrior() { std::cout << "Warrior attacks with a sword" << std::endl; }
    void moveWarrior()   { std::cout << "Warrior moves quickly" << std::endl; }
};

class Mage {
public:
    void attackMage() { std::cout << "Mage casts a spell" << std::endl; }
    void moveMage()   { std::cout << "Mage moves slowly" << std::endl; }
};

class Assassin {
public:
    void attackAssassin() { std::cout << "Assassin attacks with a dagger" << std::endl; }
    void moveAssassin()   { std::cout << "Assassin moves stealthily" << std::endl; }
};

void handleCharacterAction() {
    int characterType = 1; // 1=warrior, 2=mage, 3=assassin
    Warrior warrior; Mage mage; Assassin assassin;
    if (characterType == 1) { warrior.attackWarrior(); warrior.moveWarrior(); }
    else if (characterType == 2) { mage.attackMage(); mage.moveMage(); }
    else if (characterType == 3) { assassin.attackAssassin(); assassin.moveAssassin(); }
}
</code>

Using polymorphism, define a base

Character

with virtual

attack

and

move

methods, and let each concrete class override them:

<code>class Character {
public:
    virtual void attack() = 0;
    virtual void move() = 0;
};

class Warrior : public Character {
public:
    void attack() override { std::cout << "Warrior attacks with a sword" << std::endl; }
    void move()   override { std::cout << "Warrior moves quickly" << std::endl; }
};

class Mage : public Character {
public:
    void attack() override { std::cout << "Mage casts a spell" << std::endl; }
    void move()   override { std::cout << "Mage moves slowly" << std::endl; }
};

class Assassin : public Character {
public:
    void attack() override { std::cout << "Assassin attacks with a dagger" << std::endl; }
    void move()   override { std::cout << "Assassin moves stealthily" << std::endl; }
};

void handleCharacterAction(Character* character) {
    character->attack();
    character->move();
}

int main() {
    Warrior warrior; Mage mage; Assassin assassin;
    handleCharacterAction(&warrior);
    handleCharacterAction(&mage);
    handleCharacterAction(&assassin);
    return 0;
}
</code>

Adding a new character (e.g., Priest) only requires a new derived class; the handling function stays unchanged.

4. Polymorphism Improves Maintenance

Consider monsters with different attack and reaction logic. Without polymorphism, a large

if‑else

chain is needed:

<code>class Slime { public: void jumpAttack() { std::cout << "Slime jumps and attacks" << std::endl; } void slimeReactToAttack() { std::cout << "Slime wobbles when attacked" << std::endl; } };
class Wolf { public: void biteAttack() { std::cout << "Wolf bites and attacks" << std::endl; } void wolfReactToAttack() { std::cout << "Wolf growls when attacked" << std::endl; } };

void handleMonsterAction(int monsterType) {
    Slime slime; Wolf wolf;
    if (monsterType == 1) { slime.jumpAttack(); slime.slimeReactToAttack(); }
    else if (monsterType == 2) { wolf.biteAttack(); wolf.wolfReactToAttack(); }
}
</code>

With polymorphism, define an abstract

Monster

class:

<code>class Monster {
public:
    virtual void attack() = 0;
    virtual void reactToAttack() = 0;
};

class Slime : public Monster {
public:
    void attack() override { std::cout << "Slime jumps and attacks" << std::endl; }
    void reactToAttack() override { std::cout << "Slime wobbles when attacked" << std::endl; }
};

class Wolf : public Monster {
public:
    void attack() override { std::cout << "Wolf bites and attacks" << std::endl; }
    void reactToAttack() override { std::cout << "Wolf growls when attacked" << std::endl; }
};

void handleMonsterAction(Monster* monster) {
    monster->attack();
    monster->reactToAttack();
}
</code>

This reduces conditional logic, makes the code clearer, and eases the addition of new monster types.

5. Polymorphism in Design Patterns

(1) Strategy Pattern

The strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Polymorphism enables this separation.

<code>class OperationStrategy {
public:
    virtual double execute(double num1, double num2) = 0;
};

class AddStrategy : public OperationStrategy { public: double execute(double a, double b) override { return a + b; } };
class SubtractStrategy : public OperationStrategy { public: double execute(double a, double b) override { return a - b; } };
class MultiplyStrategy : public OperationStrategy { public: double execute(double a, double b) override { return a * b; } };
class DivideStrategy : public OperationStrategy { public: double execute(double a, double b) override { return (b != 0) ? a / b : 0; } };

class Calculator {
private:
    OperationStrategy* strategy;
public:
    Calculator(OperationStrategy* s) : strategy(s) {}
    double calculate(double a, double b) { return strategy->execute(a, b); }
};

int main() {
    OperationStrategy* add = new AddStrategy();
    Calculator calc(add);
    std::cout << "5 + 3 = " << calc.calculate(5,3) << std::endl;
    // similarly for other strategies
    delete add;
    return 0;
}
</code>

New strategies can be added without changing the

Calculator

class.

(2) Factory Method Pattern

The factory method separates object creation from usage, relying on polymorphism.

<code>class Character {
public:
    virtual void display() = 0;
};

class Warrior : public Character { public: void display() override { std::cout << "This is a warrior" << std::endl; } };
class Mage : public Character { public: void display() override { std::cout << "This is a mage" << std::endl; } };
class Assassin : public Character { public: void display() override { std::cout << "This is an assassin" << std::endl; } };

class CharacterFactory {
public:
    virtual Character* createCharacter() = 0;
};

class WarriorFactory : public CharacterFactory { public: Character* createCharacter() override { return new Warrior(); } };
class MageFactory : public CharacterFactory { public: Character* createCharacter() override { return new Mage(); } };
class AssassinFactory : public CharacterFactory { public: Character* createCharacter() override { return new Assassin(); } };

int main() {
    CharacterFactory* wf = new WarriorFactory();
    Character* w = wf->createCharacter();
    w->display();
    CharacterFactory* mf = new MageFactory();
    Character* m = mf->createCharacter();
    m->display();
    CharacterFactory* af = new AssassinFactory();
    Character* a = af->createCharacter();
    a->display();
    delete w; delete m; delete a; delete wf; delete mf; delete af;
    return 0;
}
</code>

Adding a new character type only requires a new concrete product and factory, leaving client code untouched.

design patternsC++OOPcode reusepolymorphism
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.