Visitor Design Pattern in C++



The visitor design pattern is a way of writing code that helps you in adding new features or new types of work to your classes without changing those classes again and again.

In programming, we often create classes that represent some real things like shapes, items, files, animals, or anything else. These classes usually have some basic data in them and maybe one or two simple functions. But as the project grows, we want to do more and more operations with these classes. If we keep adding all the new operations directly inside these classes, the classes become very big and hard to read. The visitor pattern helps us avoid making our classes messy.

Imagine a school building. The school admin cannot repair wires, fix water pipes, clean the classrooms, or check the furniture. Instead, different people come to the school to do these jobs. An electrician comes to fix wires. A plumber comes to fix water pipes. A cleaner comes to clean the rooms. A carpenter comes to fix furniture. The school admin allows different workers to come inside and do their job.

Visitor Design Pattern Illustration

This is the exact same idea in the visitor pattern. Your object is like the school building, and the visitor is like the worker who comes to do the job. The building has a door, and the objects have an accept method. The visitor enters through the accept method and then does the required work.

This pattern is used when you have many different objects and want to keep adding new tasks for those objects. If you try to put every single task inside the object classes, then those classes will become too large. The visitor pattern keeps things neat by moving the work into separate visitor classes.

Components of the Visitor Pattern

Following are the main components of the visitor design pattern −

  • Visitor Interface − This is like a plan that tells which types of objects the visitor can work with. It has one visit function for each element type. For example, visitCircle(), visitSquare(), visitRectangle().
  • Concrete Visitor − These are actual classes that do real work. For example, an AreaCalculator visitor will calculate area. A PerimeterCalculator visitor will calculate perimeter. A DrawingVisitor might draw the shapes on screen.
  • Element Interface − This is the main interface that all elements must follow. It usually contains only one function: accept(visitor). Every concrete element must implement this.
  • Concrete Element − These are your real objects. For example, Circle, Square, and Rectangle. They store simple data like radius or side length and implement the accept method.
  • Object Structure − This is usually a list or collection of elements. The visitor will go through this structure and visit each element one by one.
  • Client − The client sets everything up. It creates shapes, creates visitors, and asks the elements to accept the visitors.

Implementation of the Visitor Pattern in C++

Let's implement the visitor design pattern in C++ using the shape example mentioned earlier. We'll create a visitor interface for calculating area and perimeter, and concrete visitor classes for each operation.

Steps to Implement Visitor Design Pattern

Following image shows the steps to implement the visitor design pattern.

Steps to Implement Visitor Design Pattern
  • First, make a Visitor interface . This interface will have one visit function for every type of element you have. For example, visitCircle, visitSquare, visitRectangle. This tells the visitor what kinds of objects it can work with.
  • Then create Concrete Visitor classes . These are the real workers. Each visitor class does one type of job, like calculating area, perimeter, printing details, drawing shapes, or anything else you need. Inside each visitor class, you write what happens when it visits each element type.
  • After that, make an Element interface . This interface has only one important function: accept . Every element (like Circle, Square, Rectangle) must use this interface. This accept function is the “door” through which the visitor enters.
  • Next, create the Concrete Element classes . These are your actual objects. For example, Circle has radius, Square has side, Rectangle has width and height. Each one must implement the accept function. Inside accept, the element simply calls the visitor's matching visit method and passes itself.
  • Make an Object Structure . This is just a collection (like a vector, list, or array) that holds all your elements. The visitor will go through this structure and visit each element one by one. This makes it easy to run a visitor on many elements at the same time.
  • Finally, write the Client code . The client creates the objects, puts them into the structure, and creates visitors. Then the client calls accept on each element and passes the visitor. This starts the whole process and makes the visitor run its work on each element.

C++ Code for Visitor Design Pattern

In this example, we have an abstract class Shape that defines the interface for shapes like Circle , Square , and Rectangle . The Visitor interface declares visit methods for each shape type. The concrete visitor classes AreaCalculator and PerimeterCalculator implement the visit methods to calculate area and perimeter, respectively.

#include <iostream>
#include <vector>
#include <cmath>
using namespace std;

// Forward declarations
class Circle;
class Square;
class Rectangle;
// Visitor Interface
class Visitor {
   public:
      virtual void visit(Circle* c) = 0;
      virtual void visit(Square* s) = 0;
      virtual void visit(Rectangle* r) = 0;
};

// Element Interface
class Shape {
   public:
      virtual void accept(Visitor* v) = 0;
};

// Concrete Element: Circle
class Circle : public Shape {
   private:
      double radius;
   public:
      Circle(double r) : radius(r) {}
      double getRadius() { return radius; }
      void accept(Visitor* v) override {
         v->visit(this);
      }
};

// Concrete Element: Square
class Square : public Shape {
   private:
      double side;
   public:
      Square(double s) : side(s) {}
      double getSide() { return side; }
      void accept(Visitor* v) override {
         v->visit(this);
      }
};

// Concrete Element: Rectangle
class Rectangle : public Shape {
   private:
      double width, height;
   public:
      Rectangle(double w, double h) : width(w), height(h) {}
      double getWidth() { return width; }
      double getHeight() { return height; }
      void accept(Visitor* v) override {
         v->visit(this);
      }
};

// Concrete Visitor: AreaCalculator
class AreaCalculator : public Visitor {
   public:
      void visit(Circle* c) override {
         double area = M_PI * c->getRadius() * c->getRadius();
         cout << "Area of Circle: " << area << endl;
      }
      void visit(Square* s) override {
         double area = s->getSide() * s->getSide();
         cout << "Area of Square: " << area << endl;
      }
      void visit(Rectangle* r) override {
         double area = r->getWidth() * r->getHeight();
         cout << "Area of Rectangle: " << area << endl;
      }
};

// Concrete Visitor: PerimeterCalculator
class PerimeterCalculator : public Visitor {
   public:
      void visit(Circle* c) override {
         double perimeter = 2 * M_PI * c->getRadius();
         cout << "Perimeter of Circle: " << perimeter << endl;
      }
      void visit(Square* s) override {
         double perimeter = 4 * s->getSide();
         cout << "Perimeter of Square: " << perimeter << endl;
      }
      void visit(Rectangle* r) override {
         double perimeter = 2 * (r->getWidth() + r->getHeight());
         cout << "Perimeter of Rectangle: " << perimeter << endl;
      }
};

// Client Code
int main() {
   vector<Shape*> shapes;
   shapes.push_back(new Circle(5));
   shapes.push_back(new Square(4));
   shapes.push_back(new Rectangle(3, 6));

   AreaCalculator areaCalc;
   PerimeterCalculator perimeterCalc;

   for (Shape* shape : shapes) {
      shape->accept(&areaCalc);
      shape->accept(&perimeterCalc);
   }

   // Clean up
   for (Shape* shape : shapes) {
      delete shape;
   }

   return 0;
}

Following is the output of the above code −

Area of Circle: 78.5398
Perimeter of Circle: 31.4159
Area of Square: 16
Perimeter of Square: 16
Area of Rectangle: 18
Perimeter of Rectangle: 18

The output prints the area and perimeter of each shape. Important thing here is that the shapes did not calculate these values. The visitors did all the work and shapes only allowed the visitors to come in.

Pros and Cons of Visitor Design Pattern

Following are the advantages and disadvantages of using the visitor design pattern −

Pros Cons
Easy to add new work later. You can make a new visitor anytime without touching the old classes. If you add a new element type , you must update every visitor, which can be annoying.
Keeps element classes small . They only hold data and one accept function. Takes time to understand at first, because the work is not inside the element but inside the visitor.
Good when you need to do many different tasks on the same set of objects. Each task stays in its own visitor. Creates many visitor classes , which can make the project look larger if not kept organized.
Makes the project easier to maintain because the main classes rarely change. This reduces mistakes. If the shape or structure of elements changes often, then visitors break easily.

Real-World Applications of Visitor Design Pattern

Following are the real-world software use-cases of the visitor design pattern −

  • Compilers − Compilers use visitor pattern all the time because they need to walk through many different types of nodes in the syntax tree. These nodes represent things like numbers, variables, loops, conditions, functions, and more. Instead of putting checking, printing, optimization, and code generation inside every node class, they use visitors. This makes it easy to add new passes to the compiler without touching the original tree structure.
  • Document Processing − In document editors, you may have text blocks, images, tables, links, shapes, and other items. You may want to print the document, export it as PDF, count words, analyze styles, highlight mistakes, or convert it into another format. Instead of putting all these tasks into the document elements, each task becomes a visitor. This keeps the document classes simple and allows adding new features later.
  • Graphics and Game Development − Games have many objects like players, enemies, bullets, walls, items, and environment objects. You might want to render them differently, check collisions, update physics, or debug their state. Each of these actions can be a visitor. This makes the game code cleaner because you avoid stuffing every game object with too many functions. The visitor approach also helps when you want to run different “passes,” like updating health, checking triggers, or drawing outlines for debugging.
  • Data Structures − Trees, graphs, and file systems are perfect places to use visitor pattern. These structures often contain many node types, and you need to perform many operations like searching, sorting, printing, exporting, computing size, checking rules, or validating data. Visitors make it easy to run all these operations without rewriting the structure code.
  • Financial Systems − In finance, you might have many types of financial instruments like loans, bonds, stocks, insurance plans, or transactions. You may want to calculate interest, total value, tax, risk score, or generate reports. Using visitors allows each of these operations to be written separately. This also helps financial companies update formulas or add new types of calculations without editing the core classes.
  • Analytics and Reporting Tools − Many dashboards and analytics tools deal with objects like charts, graphs, metrics, logs, and events. Visitors help perform tasks like generating weekly reports, exporting data, summarizing usage, cleaning logs, or transforming data into new formats.
  • IDE Tools and Code Editors − When an editor analyses your code to show errors, warnings, auto-suggestions, refactoring options, it often uses visitors to walk through your program structure. Each feature becomes a visitor.
Real-world Software Use-cases of Visitor Design Pattern

Conclusion

In this chapter, we explored the visitor design pattern, a powerful behavioral design pattern that allows for the separation of algorithms from the objects on which they operate. We discussed the key components of the pattern, including the visitor interface, concrete visitors, element interface, and concrete elements. We also implemented the visitor pattern in C++ using a shape example, demonstrating how to calculate area and perimeter using separate visitor classes. Finally, we examined the pros and cons of the visitor pattern and highlighted its real-world applications in various domains. By using the visitor design pattern, developers can enhance code maintainability, promote separation of concerns, and easily add new operations to existing object structures.

Advertisements