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.
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
Animalclass with a virtual
makeSound()method overridden by
Dogand
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
Shapeclass 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
drawShapesfunction 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‑elsebranches.
<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
Characterwith virtual
attackand
movemethods, 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‑elsechain 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
Monsterclass:
<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
Calculatorclass.
(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.
Deepin Linux
Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.