
Application of the Visitor Pattern in C Language (Including Linux Kernel Examples)
1. Definition and Core Value of the Visitor Pattern
The Visitor Pattern is a behavioral design pattern that focuses on separating data structures from data operations, defining a visitor interface to encapsulate operations on elements within the data structure, allowing operations to be independent of changes in element types. This separation means that when new operations are needed, there is no need to modify the data structure itself; only a new visitor implementation needs to be added.
Significance: When a complex data structure (such as trees or collections) contains multiple types of elements and requires various unrelated operations (such as traversal, calculation, serialization), embedding operation logic within element classes can lead to bloated and hard-to-extend element classes (adding new operations requires modifying all element classes). The Visitor Pattern encapsulates operations as visitors, decoupling data structures from operations, aligning with the “Single Responsibility Principle” and the “Open/Closed Principle”.
Problems Addressed:
- • Strong coupling between data structures and operation logic, with element classes taking on too many responsibilities;
- • Adding new operations requires modifying all element classes, violating the “Open/Closed Principle”;
- • Multiple operations scattered across element classes, leading to poor code reusability and maintenance difficulties.
Understanding the Separation of Data Structures and Operations, Allowing Independent Changes
- •
- 1. Separation of Data Structures and Operations: Decoupling the Underlying Logic The stability of data structures (elements) is typically maintained by a set of stable element classes (such as ConcreteElementA, ConcreteElementB) and containers (such as ObjectStructure). These classes are responsible for storing data and maintaining structural relationships, rather than defining specific operation logic. For example, the structure of element nodes and attribute nodes in an XML document is determined during system design and does not change frequently. The independence of operations (visitors) is encapsulated in independent visitor classes (such as ConcreteVisitor1). Visitors operate on elements through the visit() method without modifying the internal code of the element classes. For instance, when adding operations like “serialization”, “validation”, or “format conversion” to XML nodes, only a new visitor class needs to be added without altering the element classes.
- 2. Mechanism for Independent Changes in Operations Double Dispatch is the key technology for implementing “independent changes in operations” in the Visitor Pattern. The specific process is as follows: The first dispatch: The element class calls the accept(Visitor visitor) method, passing itself (this) as a parameter to the visitor. The second dispatch: The visitor calls the corresponding visit(ConcreteElementA elementA) method based on the specific type of element received (such as ConcreteElementA) to execute the specific operation. This two-step polymorphic call ensures that the operation logic is determined by both the visitor and the element type. For example, when adding ConcreteVisitor2, it only needs to implement its own visit() method without concerning itself with the structure of the element classes; when adding ConcreteElementC, all visitor classes need to be modified to add the corresponding visit(ConcreteElementC) method (this is the main maintenance cost of the Visitor Pattern).
2. Core Idea of Implementing the Visitor Pattern in C Language
C language implements the pattern through structs encapsulating elements and visitors + function pointers defining the visit interface:
- 1. Define the Element Interface: The struct contains a
<span>accept</span>function pointer that receives the visitor and delegates the visit operation (<span>visitor->visit(self, visitor)</span>); - 2. Define the Visitor Interface: The struct contains a set of
<span>visit_xxx</span>function pointers corresponding to operations for each type of element (such as<span>visit_file</span>,<span>visit_dir</span>); - 3. Implement Concrete Elements: Each element implements the
<span>accept</span>method, calling the corresponding<span>visit_xxx</span>function of the visitor; - 4. Implement Concrete Visitors: For each operation (such as printing, counting), implement all
<span>visit_xxx</span>methods of the visitor interface, encapsulating the operation logic. - 5. The Object Structure manages the collection of elements (such as lists, trees) and provides methods to traverse elements; it can actively pass the visitor to each element to trigger the visit operation.

3. 5 Examples
Example 1: Basic Visitor Pattern (File System Traversal)
Simulate two elements “File” and “Directory” in a file system, implementing two access operations: “Print Information” and “Calculate Size”.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
// Forward declaration
typedef struct Element Element;
typedef struct Visitor Visitor;
typedef struct File File;
typedef struct Dir Dir;
// 1. Element interface: File system node
typedef struct Element {
void (*accept)(struct Element* self, Visitor* visitor);
const char* name;
} Element;
// 2. Visitor interface: Define operations on elements
typedef struct Visitor {
void (*visit_file)(struct Visitor* self, File* file); // Visit file
void (*visit_dir)(struct Visitor* self, Dir* dir); // Visit directory
} Visitor;
// 3. Concrete Element 1: File
typedef struct File {
Element base; // Inherit element interface
size_t size; // File size (bytes)
} File;
// File's accept method: Accept visitor, call visitor's visit_file
static void file_accept(Element* self, Visitor* visitor) {
visitor->visit_file(visitor, (File*)self);
}
// Create file
File* create_file(const char* name, size_t size) {
File* file = malloc(sizeof(File));
file->base.name = name;
file->base.accept = file_accept;
file->size = size;
return file;
}
// Concrete Element 2: Directory (can contain child elements)
#define MAX_CHILDREN 10
typedef struct Dir {
Element base; // Inherit element interface
Element* children[MAX_CHILDREN];
int child_count;
} Dir;
// Directory's accept method: Accept visitor, call visitor's visit_dir
static void dir_accept(Element* self, Visitor* visitor) {
visitor->visit_dir(visitor, (Dir*)self);
}
// Add child element to directory
void dir_add_child(Dir* dir, Element* child) {
if (dir->child_count < MAX_CHILDREN) {
dir->children[dir->child_count++] = child;
}
}
// Create directory
Dir* create_dir(const char* name) {
Dir* dir = malloc(sizeof(Dir));
dir->base.name = name;
dir->base.accept = dir_accept;
dir->child_count = 0;
return dir;
}
// 4. Concrete Visitor 1: Print Information Visitor
typedef struct {
Visitor base; // Inherit visitor interface
int depth; // Used for indentation to show directory level
} PrintVisitor;
// Print file information
static void print_visit_file(Visitor* self, File* file) {
PrintVisitor* pv = (PrintVisitor*)self;
// Indent by level
for (int i = 0; i < pv->depth; i++) printf(" ");
printf("File: %s (size: %zu bytes)\n", file->base.name, file->size);
}
// Print directory information (and recursively visit child elements)
static void print_visit_dir(Visitor* self, Dir* dir) {
PrintVisitor* pv = (PrintVisitor*)self;
for (int i = 0; i < pv->depth; i++) printf(" ");
printf("Dir: %s\n", dir->base.name);
// Increase level before visiting child elements
pv->depth++;
for (int i = 0; i < dir->child_count; i++) {
dir->children[i]->accept(dir->children[i], self);
}
pv->depth--;
}
// Initialize Print Visitor
PrintVisitor* create_print_visitor() {
PrintVisitor* pv = malloc(sizeof(PrintVisitor));
pv->base.visit_file = print_visit_file;
pv->base.visit_dir = print_visit_dir;
pv->depth = 0;
return pv;
}
// Concrete Visitor 2: Size Calculation Visitor
typedef struct {
Visitor base; // Inherit visitor interface
size_t total_size; // Accumulated total size
} SizeVisitor;
// Accumulate file size
static void size_visit_file(Visitor* self, File* file) {
SizeVisitor* sv = (SizeVisitor*)self;
sv->total_size += file->size;
}
// Recursively calculate the size of all elements under the directory
static void size_visit_dir(Visitor* self, Dir* dir) {
SizeVisitor* sv = (SizeVisitor*)self;
// Size of the directory itself (simulated)
sv->total_size += 4096; // Assume directory entry occupies 4KB
// Visit child elements
for (int i = 0; i < dir->child_count; i++) {
dir->children[i]->accept(dir->children[i], self);
}
}
// Initialize Size Visitor
SizeVisitor* create_size_visitor() {
SizeVisitor* sv = malloc(sizeof(SizeVisitor));
sv->base.visit_file = size_visit_file;
sv->base.visit_dir = size_visit_dir;
sv->total_size = 0;
return sv;
}
// Client usage: Build file system and apply visitors
int main() {
// Build file system structure
Dir* root = create_dir("root");
Dir* etc = create_dir("etc");
File* hosts = create_file("hosts", 1024);
File* passwd = create_file("passwd", 2048);
dir_add_child(etc, (Element*)hosts);
dir_add_child(etc, (Element*)passwd);
File* README = create_file("README.txt", 512);
dir_add_child(root, (Element*)etc);
dir_add_child(root, (Element*)README);
// Use Print Visitor
printf("--- File System Structure ---\n");
PrintVisitor* print_vis = create_print_visitor();
root->base.accept((Element*)root, (Visitor*)print_vis);
// Use Size Visitor
printf("\n--- Total Size Calculation ---\n");
SizeVisitor* size_vis = create_size_visitor();
root->base.accept((Element*)root, (Visitor*)size_vis);
printf("Total size: %zu bytes\n", size_vis->total_size);
// Free resources
free(print_vis);
free(size_vis);
free(README);
free(passwd);
free(hosts);
free(etc);
free(root);
return 0;
}
The output of the above code is as follows
--- File System Structure ---
Dir: root
Dir: etc
File: hosts (size: 1024 bytes)
File: passwd (size: 2048 bytes)
File: README.txt (size: 512 bytes)
--- Total Size Calculation ---
Total size: 11776 bytes
Its UML diagram is as follows

That is: “File” and “Directory” in the file system are elements, while “Print Information” and “Calculate Size” are visitors. Elements receive visitors through the <span>accept</span> method, and the visitor executes corresponding operations based on the element type. Adding new operations (such as “Search Content”) only requires adding a new visitor without modifying the code of files or directories.
Example 2: Graphical Element Access (Shape Drawing and Area Calculation)
Simulate graphical elements such as circles and rectangles, implementing two access operations: “Draw” and “Calculate Area”.
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
// Forward declaration
typedef struct Shape Shape;
typedef struct Visitor Visitor;
typedef struct Circle Circle;
typedef struct Rectangle Rectangle;
// 1. Element interface: Shape
typedef struct Shape {
void (*accept)(struct Shape* self, Visitor* visitor);
} Shape;
// 2. Visitor interface: Shape operations
typedef struct Visitor {
void (*visit_circle)(struct Visitor* self, Circle* circle);
void (*visit_rectangle)(struct Visitor* self, Rectangle* rect);
} Visitor;
// 3. Concrete Element 1: Circle
typedef struct Circle {
Shape base;
float x, y; // Center coordinates
float radius; // Radius
} Circle;
static void circle_accept(Shape* self, Visitor* visitor) {
visitor->visit_circle(visitor, (Circle*)self);
}
Circle* create_circle(float x, float y, float radius) {
Circle* circle = malloc(sizeof(Circle));
circle->base.accept = circle_accept;
circle->x = x;
circle->y = y;
circle->radius = radius;
return circle;
}
// Concrete Element 2: Rectangle
typedef struct Rectangle {
Shape base;
float x, y; // Top-left coordinates
float width, height; // Width and height
} Rectangle;
static void rect_accept(Shape* self, Visitor* visitor) {
visitor->visit_rectangle(visitor, (Rectangle*)self);
}
Rectangle* create_rectangle(float x, float y, float w, float h) {
Rectangle* rect = malloc(sizeof(Rectangle));
rect->base.accept = rect_accept;
rect->x = x;
rect->y = y;
rect->width = w;
rect->height = h;
return rect;
}
// 4. Concrete Visitor 1: Draw Visitor
typedef struct {
Visitor base;
} DrawVisitor;
static void draw_circle(Visitor* self, Circle* circle) {
(void)self;
printf("Drawing Circle: center(%.1f, %.1f), radius %.1f\n",
circle->x, circle->y, circle->radius);
}
static void draw_rectangle(Visitor* self, Rectangle* rect) {
(void)self;
printf("Drawing Rectangle: top-left(%.1f, %.1f), w=%.1f, h=%.1f\n",
rect->x, rect->y, rect->width, rect->height);
}
DrawVisitor* create_draw_visitor() {
DrawVisitor* dv = malloc(sizeof(DrawVisitor));
dv->base.visit_circle = draw_circle;
dv->base.visit_rectangle = draw_rectangle;
return dv;
}
// Concrete Visitor 2: Area Calculation Visitor
typedef struct {
Visitor base;
float total_area; // Accumulated area
} AreaVisitor;
static void area_circle(Visitor* self, Circle* circle) {
AreaVisitor* av = (AreaVisitor*)self;
float area = M_PI * circle->radius * circle->radius;
av->total_area += area;
printf("Circle area: %.2f\n", area);
}
static void area_rectangle(Visitor* self, Rectangle* rect) {
AreaVisitor* av = (AreaVisitor*)self;
float area = rect->width * rect->height;
av->total_area += area;
printf("Rectangle area: %.2f\n", area);
}
AreaVisitor* create_area_visitor() {
AreaVisitor* av = malloc(sizeof(AreaVisitor));
av->base.visit_circle = area_circle;
av->base.visit_rectangle = area_rectangle;
av->total_area = 0;
return av;
}
// Client usage: Create shapes and apply visitors
int main() {
Shape* shapes[] = {
(Shape*)create_circle(10, 10, 5),
(Shape*)create_rectangle(20, 20, 10, 8),
(Shape*)create_circle(30, 30, 3)
};
int shape_count = sizeof(shapes)/sizeof(shapes[0]);
// Draw all shapes
printf("--- Drawing Shapes ---\n");
DrawVisitor* draw_vis = create_draw_visitor();
for (int i = 0; i < shape_count; i++) {
shapes[i]->accept(shapes[i], (Visitor*)draw_vis);
}
// Calculate total area
printf("\n--- Calculating Areas ---\n");
AreaVisitor* area_vis = create_area_visitor();
for (int i = 0; i < shape_count; i++) {
shapes[i]->accept(shapes[i], (Visitor*)area_vis);
}
printf("Total area: %.2f\n", area_vis->total_area);
// Free resources
free(draw_vis);
free(area_vis);
for (int i = 0; i < shape_count; i++) {
free(shapes[i]);
}
return 0;
}
The output of the above code is as follows
--- Drawing Shapes ---
Drawing Circle: center(10.0, 10.0), radius 5.0
Drawing Rectangle: top-left(20.0, 20.0), w=10.0, h=8.0
Drawing Circle: center(30.0, 30.0), radius 3.0
--- Calculating Areas ---
Circle area: 78.54
Rectangle area: 80.00
Circle area: 28.27
Total area: 186.81
Its UML diagram is as follows

That is: Circles and rectangles are graphical elements, while drawing and area calculation are visitors. Elements inform the visitor of their type through the <span>accept</span> method, and the visitor executes corresponding operations based on the type. Adding new operations (such as “Calculate Perimeter”) only requires adding a new visitor, adhering to the “Open/Closed Principle”.
Example 3: AST Node Access (Abstract Syntax Tree Operations)
Simulate “Number” and “Addition” nodes in a programming language AST (Abstract Syntax Tree), implementing two access operations: “Evaluate” and “Print Expression”.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Forward declaration
typedef struct ASTNode ASTNode;
typedef struct Visitor Visitor;
typedef struct Number Number;
typedef struct Add Add;
// 1. Element interface: AST Node
typedef struct ASTNode {
void (*accept)(struct ASTNode* self, Visitor* visitor);
} ASTNode;
// 2. Visitor interface: AST Operations
typedef struct Visitor {
int (*visit_number)(struct Visitor* self, Number* num);
int (*visit_add)(struct Visitor* self, Add* add);
} Visitor;
// 3. Concrete Element 1: Number Node
typedef struct Number {
ASTNode base;
int value; // Number value
} Number;
static void number_accept(ASTNode* self, Visitor* visitor) {
// The visit result of the number node is its value
visitor->visit_number(visitor, (Number*)self);
}
Number* create_number(int value) {
Number* num = malloc(sizeof(Number));
num->base.accept = number_accept;
num->value = value;
return num;
}
// Concrete Element 2: Addition Node (contains left and right child nodes)
typedef struct Add {
ASTNode base;
ASTNode* left; // Left operand
ASTNode* right; // Right operand
} Add;
static void add_accept(ASTNode* self, Visitor* visitor) {
// The visit result of the addition node is the sum of the evaluated child nodes
visitor->visit_add(visitor, (Add*)self);
}
Add* create_add(ASTNode* left, ASTNode* right) {
Add* add = malloc(sizeof(Add));
add->base.accept = add_accept;
add->left = left;
add->right = right;
return add;
}
// 4. Concrete Visitor 1: Evaluation Visitor (calculates the expression result)
typedef struct {
Visitor base;
int result; // Stores the evaluation result of the current node
} EvalVisitor;
static int eval_number(Visitor* self, Number* num) {
EvalVisitor* ev = (EvalVisitor*)self;
ev->result = num->value;
return ev->result;
}
static int eval_add(Visitor* self, Add* add) {
EvalVisitor* ev = (EvalVisitor*)self;
// Recursively evaluate the left subtree
add->left->accept(add->left, self);
int left_val = ev->result;
// Recursively evaluate the right subtree
add->right->accept(add->right, self);
int right_val = ev->result;
// Calculate the sum
ev->result = left_val + right_val;
return ev->result;
}
EvalVisitor* create_eval_visitor() {
EvalVisitor* ev = malloc(sizeof(EvalVisitor));
ev->base.visit_number = eval_number;
ev->base.visit_add = eval_add;
ev->result = 0;
return ev;
}
// Concrete Visitor 2: Print Visitor (outputs the expression string)
typedef struct {
Visitor base;
char buffer[256]; // Stores the string representation of the current node
} PrintVisitor;
static int print_number(Visitor* self, Number* num) {
PrintVisitor* pv = (PrintVisitor*)self;
snprintf(pv->buffer, sizeof(pv->buffer), "%d", num->value);
return 0;
}
static int print_add(Visitor* self, Add* add) {
PrintVisitor* pv = (PrintVisitor*)self;
// Recursively get the left subtree string
add->left->accept(add->left, self);
char left_buf[128];
strncpy(left_buf, pv->buffer, sizeof(left_buf));
// Recursively get the right subtree string
add->right->accept(add->right, self);
char right_buf[128];
strncpy(right_buf, pv->buffer, sizeof(right_buf));
// Concatenate to form "(left + right)"
snprintf(pv->buffer, sizeof(pv->buffer), "(%s + %s)", left_buf, right_buf);
return 0;
}
PrintVisitor* create_print_visitor() {
PrintVisitor* pv = malloc(sizeof(PrintVisitor));
pv->base.visit_number = print_number;
pv->base.visit_add = print_add;
pv->buffer[0] = '\0';
return pv;
}
// Client usage: Build AST and apply visitors
int main() {
// Build AST: 1 + (2 + 3)
ASTNode* num1 = (ASTNode*)create_number(1);
ASTNode* num2 = (ASTNode*)create_number(2);
ASTNode* num3 = (ASTNode*)create_number(3);
ASTNode* add23 = (ASTNode*)create_add(num2, num3);
ASTNode* root = (ASTNode*)create_add(num1, add23);
// Print expression
printf("--- Expression ---\n");
PrintVisitor* print_vis = create_print_visitor();
root->accept(root, (Visitor*)print_vis);
printf("%s\n", print_vis->buffer);
// Calculate expression result
printf("\n--- Evaluation ---\n");
EvalVisitor* eval_vis = create_eval_visitor();
root->accept(root, (Visitor*)eval_vis);
printf("Result: %d\n", eval_vis->result);
// Free resources
free(print_vis);
free(eval_vis);
free(root); // add1
free(add23); // add23
free(num1);
free(num2);
free(num3);
return 0;
}
The output of the above code is as follows
--- Expression ---
(1 + (2 + 3))
--- Evaluation ---
Result: 6
Its UML diagram is as follows

That is: The number and addition nodes in the AST are elements, while evaluation and printing are visitors. Visitors implement complex operations by recursively visiting child nodes, and adding new operations (such as “Code Generation”) only requires adding a new visitor without modifying the AST node structure, making it a typical design pattern in the front end of compilers.
Example 4: Device Access (Hardware Device Operations)
Simulate hardware devices such as sensors and actuators, implementing two access operations: “Read Data” and “Configure Parameters”.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
// Forward declaration
typedef struct Device Device;
typedef struct Visitor Visitor;
typedef struct Sensor Sensor;
typedef struct Actuator Actuator;
// 1. Element interface: Device
typedef struct Device {
void (*accept)(struct Device* self, Visitor* visitor);
const char* name;
} Device;
// 2. Visitor interface: Device operations
typedef struct Visitor {
void (*visit_sensor)(struct Visitor* self, Sensor* sensor);
void (*visit_actuator)(struct Visitor* self, Actuator* actuator);
} Visitor;
// 3. Concrete Element 1: Sensor (can read data)
typedef struct Sensor {
Device base;
float value; // Current reading
float min, max; // Range
} Sensor;
static void sensor_accept(Device* self, Visitor* visitor) {
visitor->visit_sensor(visitor, (Sensor*)self);
}
Sensor* create_sensor(const char* name, float min, float max) {
Sensor* sensor = malloc(sizeof(Sensor));
sensor->base.name = name;
sensor->base.accept = sensor_accept;
sensor->value = 0;
sensor->min = min;
sensor->max = max;
return sensor;
}
// Concrete Element 2: Actuator (can receive control commands)
typedef struct Actuator {
Device base;
bool is_active; // Is active
int power; // Power (0-100)
} Actuator;
static void actuator_accept(Device* self, Visitor* visitor) {
visitor->visit_actuator(visitor, (Actuator*)self);
}
Actuator* create_actuator(const char* name) {
Actuator* actuator = malloc(sizeof(Actuator));
actuator->base.name = name;
actuator->base.accept = actuator_accept;
actuator->is_active = false;
actuator->power = 0;
return actuator;
}
// 4. Concrete Visitor 1: Data Reading Visitor
typedef struct {
Visitor base;
} ReadVisitor;
static void read_sensor(Visitor* self, Sensor* sensor) {
(void)self;
// Simulate reading sensor data (random value)
sensor->value = sensor->min + (rand() % (int)((sensor->max - sensor->min) * 10)) / 10.0f;
printf("Read %s: %.2f (range: %.2f-%.2f)\n",
sensor->base.name, sensor->value, sensor->min, sensor->max);
}
static void read_actuator(Visitor* self, Actuator* actuator) {
(void)self;
printf("Read %s: active=%s, power=%d%%\n",
actuator->base.name, actuator->is_active ? "true" : "false", actuator->power);
}
ReadVisitor* create_read_visitor() {
ReadVisitor* rv = malloc(sizeof(ReadVisitor));
rv->base.visit_sensor = read_sensor;
rv->base.visit_actuator = read_actuator;
return rv;
}
// Concrete Visitor 2: Configuration Visitor
typedef struct {
Visitor base;
int target_power; // Target power to set
} ConfigVisitor;
static void config_sensor(Visitor* self, Sensor* sensor) {
(void)self;
// Simulate configuring sensor range
sensor->min -= 1.0f;
sensor->max += 1.0f;
printf("Configured %s: new range %.2f-%.2f\n",
sensor->base.name, sensor->min, sensor->max);
}
static void config_actuator(Visitor* self, Actuator* actuator) {
ConfigVisitor* cv = (ConfigVisitor*)self;
// Configure actuator power
actuator->power = cv->target_power;
actuator->is_active = (cv->target_power > 0);
printf("Configured %s: power set to %d%% (active=%s)\n",
actuator->base.name, actuator->power, actuator->is_active ? "true" : "false");
}
ConfigVisitor* create_config_visitor(int target_power) {
ConfigVisitor* cv = malloc(sizeof(ConfigVisitor));
cv->base.visit_sensor = config_sensor;
cv->base.visit_actuator = config_actuator;
cv->target_power = target_power;
return cv;
}
// Client usage: Manage devices and apply visitors
int main() {
Device* devices[] = {
(Device*)create_sensor("Temperature", 0.0f, 100.0f),
(Device*)create_actuator("Motor"),
(Device*)create_sensor("Pressure", 0.0f, 5.0f)
};
int device_count = sizeof(devices)/sizeof(devices[0]);
// Read data from all devices
printf("--- Reading Devices ---\n");
ReadVisitor* read_vis = create_read_visitor();
for (int i = 0; i < device_count; i++) {
devices[i]->accept(devices[i], (Visitor*)read_vis);
}
// Configure all devices
printf("\n--- Configuring Devices ---\n");
ConfigVisitor* config_vis = create_config_visitor(70);
for (int i = 0; i < device_count; i++) {
devices[i]->accept(devices[i], (Visitor*)config_vis);
}
// Read again to verify configuration
printf("\n--- Reading After Config ---\n");
for (int i = 0; i < device_count; i++) {
devices[i]->accept(devices[i], (Visitor*)read_vis);
}
// Free resources
free(read_vis);
free(config_vis);
for (int i = 0; i < device_count; i++) {
free(devices[i]);
}
return 0;
}
The output of the above code is as follows
--- Reading Devices ---
Read Temperature: 38.30 (range: 0.00-100.00)
Read Motor: active=false, power=0%
Read Pressure: 3.60 (range: 0.00-5.00)
--- Configuring Devices ---
Configured Temperature: new range -1.00-101.00
Configured Motor: power set to 70% (active=true)
Configured Pressure: new range -1.00-6.00
--- Reading After Config ---
Read Temperature: 40.70 (range: -1.00-101.00)
Read Motor: active=true, power=70%
Read Pressure: -0.50 (range: -1.00-6.00)
Its UML diagram is as follows

That is: Sensors and actuators are device elements, while reading and configuring are visitors. Visitors perform specific operations for different device types, and adding a new device type (such as “Display”) only requires implementing the <span>accept</span> method and adding <span>visit_display</span> in the visitor, meeting the design requirements of the hardware abstraction layer.
Example 5: Kernel-Style Visitor (Process Resource Traversal)
Simulate the “File Descriptor” and “Memory Segment” resources of Linux kernel processes, implementing two access operations: “Count Resources” and “Release Resources”.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
// Forward declaration
typedef struct Resource Resource;
typedef struct Visitor Visitor;
typedef struct FileDesc FileDesc;
typedef struct MemSegment MemSegment;
typedef struct Process Process;
// 1. Element interface: Process Resource
typedef struct Resource {
void (*accept)(struct Resource* self, Visitor* visitor);
const char* type; // Resource type
} Resource;
// 2. Visitor interface: Resource operations
typedef struct Visitor {
void (*visit_file)(struct Visitor* self, FileDesc* fd);
void (*visit_mem)(struct Visitor* self, MemSegment* mem);
} Visitor;
// 3. Concrete Element 1: File Descriptor
typedef struct FileDesc {
Resource base;
int fd; // File descriptor number
const char* path; // Associated path
} FileDesc;
static void fd_accept(Resource* self, Visitor* visitor) {
visitor->visit_file(visitor, (FileDesc*)self);
}
FileDesc* create_file_desc(int fd, const char* path) {
FileDesc* f = malloc(sizeof(FileDesc));
f->base.type = "file";
f->base.accept = fd_accept;
f->fd = fd;
f->path = path;
return f;
}
// Concrete Element 2: Memory Segment
typedef struct MemSegment {
Resource base;
uintptr_t start; // Start address
size_t size; // Size (bytes)
const char* name; // Segment name (e.g., "stack", "heap")
} MemSegment;
static void mem_accept(Resource* self, Visitor* visitor) {
visitor->visit_mem(visitor, (MemSegment*)self);
}
MemSegment* create_mem_segment(uintptr_t start, size_t size, const char* name) {
MemSegment* m = malloc(sizeof(MemSegment));
m->base.type = "memory";
m->base.accept = mem_accept;
m->start = start;
m->size = size;
m->base.name = name;
return m;
}
// Process (contains multiple resources)
#define MAX_RESOURCES 10
typedef struct Process {
pid_t pid;
Resource* resources[MAX_RESOURCES];
int res_count;
} Process;
Process* create_process(pid_t pid) {
Process* p = malloc(sizeof(Process));
p->pid = pid;
p->res_count = 0;
return p;
}
void process_add_resource(Process* p, Resource* res) {
if (p->res_count < MAX_RESOURCES) {
p->resources[p->res_count++] = res;
}
}
// 4. Concrete Visitor 1: Resource Statistics Visitor
typedef struct {
Visitor base;
int file_count; // Total number of files
size_t total_mem; // Total memory (bytes)
} StatVisitor;
static void stat_file(Visitor* self, FileDesc* fd) {
StatVisitor* sv = (StatVisitor*)self;
sv->file_count++;
printf("Process resource: File FD=%d, Path=%s\n", fd->fd, fd->path);
}
static void stat_mem(Visitor* self, MemSegment* mem) {
StatVisitor* sv = (StatVisitor*)self;
sv->total_mem += mem->size;
printf("Process resource: Mem 0x%lx-%lx, Size=%zuKB, Name=%s\n",
mem->start, mem->start + mem->size - 1, mem->size / 1024, mem->name);
}
StatVisitor* create_stat_visitor() {
StatVisitor* sv = malloc(sizeof(StatVisitor));
sv->base.visit_file = stat_file;
sv->base.visit_mem = stat_mem;
sv->file_count = 0;
sv->total_mem = 0;
return sv;
}
// Concrete Visitor 2: Resource Release Visitor
typedef struct {
Visitor base;
pid_t target_pid; // Target process PID
} ReleaseVisitor;
static void release_file(Visitor* self, FileDesc* fd) {
(void)self;
printf("Released file resource: FD=%d\n", fd->fd);
}
static void release_mem(Visitor* self, MemSegment* mem) {
(void)self;
printf("Released memory resource: 0x%lx (size %zuKB)\n",
mem->start, mem->size / 1024);
}
ReleaseVisitor* create_release_visitor(pid_t pid) {
ReleaseVisitor* rv = malloc(sizeof(ReleaseVisitor));
rv->base.visit_file = release_file;
rv->base.visit_mem = release_mem;
rv->target_pid = pid;
return rv;
}
// Client usage (simulate kernel process management)
int main() {
// Create process and add resources
Process* proc = create_process(1234);
process_add_resource(proc, (Resource*)create_file_desc(0, "/dev/stdin"));
process_add_resource(proc, (Resource*)create_file_desc(1, "/dev/stdout"));
process_add_resource(proc, (Resource*)create_mem_segment(0x7f000000, 4096*1024, "heap"));
process_add_resource(proc, (Resource*)create_mem_segment(0x7ff00000, 1024*1024, "stack"));
// Statistics of process resources
printf("--- Process %d Resources ---\n", proc->pid);
StatVisitor* stat_vis = create_stat_visitor();
for (int i = 0; i < proc->res_count; i++) {
proc->resources[i]->accept(proc->resources[i], (Visitor*)stat_vis);
}
printf("Summary: %d files, Total memory: %zuKB\n",
stat_vis->file_count, stat_vis->total_mem / 1024);
// Release process resources
printf("\n--- Releasing Resources for PID %d ---\n", proc->pid);
ReleaseVisitor* release_vis = create_release_visitor(proc->pid);
for (int i = 0; i < proc->res_count; i++) {
proc->resources[i]->accept(proc->resources[i], (Visitor*)release_vis);
}
// Free resources
free(stat_vis);
free(release_vis);
for (int i = 0; i < proc->res_count; i++) {
free(proc->resources[i]);
}
free(proc);
return 0;
}
The output of the above code is as follows
--- Process 1234 Resources ---
Process resource: File FD=0, Path=/dev/stdin
Process resource: File FD=1, Path=/dev/stdout
Process resource: Mem 0x7f000000-7f3fffff, Size=4096KB, Name=heap
Process resource: Mem 0x7ff00000-7fffffff, Size=1024KB, Name=stack
Summary: 2 files, Total memory: 5120KB
--- Releasing Resources for PID 1234 ---
Released file resource: FD=0
Released file resource: FD=1
Released memory resource: 0x7f000000 (size 4096KB)
Released memory resource: 0x7ff00000 (size 1024KB)
Its UML diagram is as follows

That is: Simulating resource management of Linux kernel processes, file descriptors and memory segments are resource elements, while statistics and release are visitors. Visitors traverse process resources and execute operations, similar to the resource management logic in <span>task_struct</span> in the kernel, and adding a new resource type (such as “Semaphore”) only requires extending the element and visitor interfaces.
4. Application of the Visitor Pattern in the Linux Kernel
- 1. Device Model Traversal (
<span>kobject</span><span> and </span><code><span>kset</span><span>)</span>: In the kernel device model,<span>kobject</span><span> (device object) and </span><code><span>kset</span><span> (object collection) form a hierarchical structure, and functions like </span><code><span>kobject_foreach_child</span><span> can be seen as the skeleton of a visitor, with specific operations (such as reading and writing </span><code><span>sysfs</span><span> attributes) implemented through callback functions (visitors), avoiding modification of the core device model code.</span> - 2. File System Inode Traversal:
<span>inode</span><span> is the core element of the file system (including files, directories, links, etc.), and functions like </span><code><span>iterate_dir</span><span> handle different types of inodes through the </span><code><span>file_operations->iterate</span><span> callback (visitor), implementing directory traversal and other operations without modifying the </span><code><span>inode</span><span> structure itself.</span> - 3. Process Resource Reclamation (
<span>exit_files</span><span>/</span><code><span>exit_mm</span><span>)</span>: When a process exits, the kernel needs to reclaim resources such as file descriptors and memory segments. The<span>do_exit</span><span> function serves as a template method, reclaiming different types of resources through functions like </span><code><span>exit_files</span><span> (file resource visitor) and </span><code><span>exit_mm</span><span> (memory resource visitor), decoupling resource reclamation logic from the process structure.</span> - 4. Network Packet Processing (
<span>sk_buff</span><span>)</span>: In the network protocol stack,<span>sk_buff</span><span> (socket buffer) needs to be processed through the link layer, network layer, and transport layer. The processing functions for each protocol (such as </span><code><span>ip_rcv</span><span>, </span><code><span>tcp_rcv</span><span>) can be seen as visitors, executing corresponding operations based on the packet type, and adding a new protocol only requires implementing a new visitor function.</span> - 5. Debug Information Collection (
<span>debugfs</span><span>)</span>:<span>debugfs</span><span> provides kernel debugging interfaces, and different kernel components (such as processes, memory, networks) expose debugging information by registering </span><code><span>debugfs</span><span> files and operation functions (visitors), with the </span><code><span>debugfs</span><span> framework serving as an element container, without needing to care about the implementation details of specific components.</span>
5. Implementation Considerations
- 1. Matching Element Types with Visitor Methods: When adding new element types, corresponding
<span>visit_xxx</span><span> methods must be added in all visitors, otherwise it may lead to undefined behavior (such as null pointer calls). In C language, this can be enforced through macro definitions or compiler checks.</span> - 2. Avoid Circular Dependencies between Elements and Visitors: Elements and visitors should interact through interfaces as much as possible, avoiding direct references to each other’s concrete implementations, as this can lead to tight coupling and difficulty in independent extension.
- 3. Risk of Stack Overflow in Recursive Access: For tree structures (such as directories, AST), visitors may recursively access child elements, so attention should be paid to stack depth; complex structures should implement access iteratively.
- 4. Thread Safety: In a multithreaded environment, if visitors modify shared data (such as statistical counters), mutual exclusion locks should be used for protection; if the state of elements may change during access (such as dynamically adding/removing child elements), the atomicity of traversal should be ensured.
- 5. Performance Considerations: The Visitor Pattern may introduce slight performance overhead due to indirect function calls through function pointers; high-frequency access scenarios (such as kernel hot paths) need to weigh flexibility against performance, and inline or compile-time binding optimizations may be necessary.
6. Additional Notes
- • The difference between the Visitor Pattern and the Iterator Pattern: The Visitor Pattern focuses on performing different operations on different types of elements, while the Iterator Pattern focuses on traversing container elements without exposing internal structures. The two are often used together (the iterator traverses elements, and the visitor processes elements).
- • Applicability of the Visitor Pattern: It is suitable for scenarios where element types are stable but operations are variable (such as AST, device models); if element types change frequently (requiring frequent additions of elements), the Visitor Pattern will require modifications to all visitors, in which case it should be avoided.
- • Extension forms: A “double dispatch” mechanism can be implemented, where both the element type and the visitor type jointly determine the operation to be executed. In C language, this can be achieved through nested function pointer calls (e.g.,
<span>element->accept(element, visitor)</span><span> internally calls </span><code><span>visitor->visit_element(visitor, element)</span><span>).</span>
Through the Visitor Pattern, C language programs (especially in the Linux kernel) can flexibly extend operation logic on complex data structures, achieving decoupling of data and operations, significantly enhancing system maintainability and scalability. Many core components in the kernel adopt this pattern, reflecting the design philosophy of “separation of concerns”.
Click to read the full article, thank you for sharing, saving, liking, and viewing