C++
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented C++
-
Section 4: Memory Management
-
Section 5: Templates and Generic Programming
-
Section 6: The Standard Template Library
-
Section 7: Modern C++ Features
-
Section 8: Error Handling
-
Section 9: Multithreading
-
Section 10: Operator Overloading and Type Conversion
-
Section 11: Advanced Topics
-
Section 12: Tooling and Build Systems
-
Section 13: Design Patterns in C++
-
Section 14: Interfacing with C and Systems Programming
-
Section 15: Networking and IPC Basics
-
Section 16: Graphics and Game Programming Basics
-
Section 17: The Boost Libraries
-
Section 18: Data Structures and Algorithms in C++
-
Section 19: Practical Projects
-
Section 20: More Concurrency Patterns
-
Section 21: More OOP and Design Practice
-
Section 22: File I/O and Streams
-
Section 23: More Standard Library
-
Section 24: Practice Exercises
-
Section 25: Interview and Algorithm Practice
-
Section 26: Compiler and Language Internals
-
Section 27: GUI and Application Frameworks Overview
-
Section 28: Testing and Quality Practices
-
Section 29: Numerics Library
-
Section 30: Concepts Library (C++20)
-
Section 31: Ranges Library (C++20) In Depth
-
Section 32: More Utility Library
140: The Visitor Pattern
I was working on a small game engine project recently, and I hit a wall that I think you'll recognize. I had a hierarchy of game elements: Player, Enemy, and Prop. They all inherited from a base GameObject. Initially, it was simple. I just added a draw() virtual function to the base class, and each subclass implemented its own rendering logic.
The Bloated Base Class Problem
Everything felt fine until I started adding more features. First, I needed to save the game state, so I added saveToJson() to the base class. Then I needed a way to calculate collision bounds, so I added calculateBounds(). Then I wanted a debug logger, so I added logState().
// This is where I was a few weeks ago
class GameObject {
public:
virtual void draw() = 0;
virtual void saveToJson() = 0;
virtual void calculateBounds() = 0;
virtual void logState() = 0;
virtual ~GameObject() = default;
};
class Player : public GameObject {
void draw() override { /* Player draw logic */ }
void saveToJson() override { /* Player save logic */ }
void calculateBounds() override { /* Player bounds logic */ }
void logState() override { /* Player log logic */ }
};
// ... and the same for Enemy and Prop
I stopped and looked at my Player class and realized it was becoming a dumping ground. Every time I wanted to add a new operation to the game, I had to modify every single class in the hierarchy. If I have 20 types of game objects and I want to add a "Export to XML" feature, I'm touching 21 files. That's a maintenance nightmare and a direct violation of the Open/Closed Principle.
Trying to Externalize the Logic
I thought, "Why not just move the logic into a separate class?" I tried creating a GameExporter class that took a GameObject*. But then I ran into a classic C++ problem: polymorphism only works one way.
class GameExporter {
public:
void exportObject(GameObject* obj) {
// Wait, how do I know if this is a Player or an Enemy?
// I can't just call obj->exportLogic() because that's exactly
// what I'm trying to move OUT of the GameObject class.
}
};
My first instinct was to use dynamic_cast. I started writing a chain of if (auto p = dynamic_cast<Player*(obj)>). I deleted it immediately. Using dynamic_cast for core logic is slow, ugly, and brittle. If I add a new NPC class, I have to remember to go back to the GameExporter and add another if statement. I was just moving the problem from one place to another.
The Double Dispatch Trick
This is where the Visitor pattern comes in. The goal is to separate the algorithm from the object structure. The trick is something called "Double Dispatch."
Instead of the visitor trying to guess the type of the object, the object tells the visitor exactly what it is. I'll add one single method to my base class: accept(). This method doesn't do any work; it just hands the object back to the visitor.
class Player;
class Enemy;
class Prop;
// The "Visitor" interface defines an operation for every concrete type
class GameObjectVisitor {
public:
virtual void visit(Player* player) = 0;
virtual void visit(Enemy* enemy) = 0;
virtual void visit(Prop* prop) = 0;
virtual ~GameObjectVisitor() = default;
};
class GameObject {
public:
// The only method we'll ever need to add to the hierarchy for new operations
virtual void accept(GameObjectVisitor* visitor) = 0;
virtual ~GameObject() = default;
};
class Player : public GameObject {
public:
void accept(GameObjectVisitor* visitor) override {
visitor->visit(this); // 'this' is known to be a Player* here!
}
};
class Enemy : public GameObject {
public:
void accept(GameObjectVisitor* visitor) override {
visitor->visit(this); // 'this' is known to be an Enemy* here!
}
};
Look at what just happened in accept(). When Player::accept is called, the compiler knows for a fact that this is a Player*. It then calls visitor->visit(this), which resolves to the visit(Player* player) overload in the visitor. We've dispatched twice: first to the object's accept method, then back to the visitor's visit method.
Putting it to Work
Now, if I want to add a "Save to JSON" feature, I don't touch the Player or Enemy classes at all. I just create a new visitor.
class JsonExportVisitor : public GameObjectVisitor {
public:
void visit(Player* player) override {
std::cout <"Exporting player data to JSON..."> std::endl;
}
void visit(Enemy* enemy) override {
std::cout <"Exporting enemy data to JSON..."> std::endl;
}
void visit(Prop* prop) override {
std::cout <"Exporting prop data to JSON..."> std::endl;
}
};
// Usage
std::vector<GameObject*> scene = { new Player(), new Enemy(), new Prop() };
JsonExportVisitor exporter;
for (auto obj : scene) {
obj->accept(&exporter);
}
I love this because my GameObject classes stay lean. They only care about their own data. The logic for saving, rendering, or AI is encapsulated in its own visitor class. If I want to add a XmlExportVisitor tomorrow, I just write the new class and I'm done. No existing code is modified, which means I can't accidentally break the Player class while trying to implement XML export.
📋 Practical Task
Implementing a Combat Damage Visitor
You are building a combat system for a tactical RPG. You have three types of units: Warrior, Mage, and Archer. Each unit has a different way of receiving damage based on their armor and resistances.
Your Task:
- Create a
Unitbase class and three derived classes:Warrior,Mage, andArcher. - Implement the Visitor pattern so that you can apply damage without adding a
takeDamage()method that contains specific logic inside every unit class. - Create a
DamageVisitorthat implements different damage calculations:- Warriors reduce damage by 50%.
- Mages take 2x damage.
- Archers take normal damage.
- In your
main()function, create astd::vectorofUnit*containing one of each type and use theDamageVisitorto process them.
There are no comments for now.