个人专著《C++元编程与通用设计模式实现》由清华大学出版社出版。该书内容源于工业级项目实践,出版后市场反馈积极(已加印)。其专业价值获得了图书馆系统的广泛认可:不仅被中国国家图书馆作为流通与保存本收藏,还被近半数省级公共图书馆及清华大学、浙江大学等超过35所高校图书馆收录为馆藏。

个人软仓,gitee搜索“galaxy_0”

深入解析访问者模式:一个C++模板实现的通用访问者库

1. 访问者模式简介

在软件开发中,我们经常需要对一组对象执行一系列不同的操作,而这些操作可能随着需求变化而频繁扩展。传统的做法是在每个对象类中添加新的方法,但这会破坏类的封装性,并且每增加一个新操作就要修改所有相关类,违反了开闭原则。访问者模式(Visitor Pattern) 正是为了解决这一问题而生的——它表示一个作用于某对象结构中的各元素的操作,使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。

访问者模式的核心思想是将操作与对象结构分离,让操作独立变化。它适用于数据结构相对稳定但操作多变的场景,比如编译器中的抽象语法树(AST)遍历、图形系统中的形状处理等。

今天我们要介绍的是一个基于C++模板实现的通用访问者类库。它通过将操作函数与名称绑定,提供了一种灵活的方式来注册和调用对不同数据对象的处理。你可以动态添加、移除操作,并通过名称选择要执行的操作,非常适合需要动态扩展功能的场景。

2. 代码设计解析

2.1 整体设计思路

这个访问者框架的核心思想是:用字符串作为操作的标识,将对应的处理函数存储在一个映射表中,客户端通过操作名来调用相应的处理逻辑。它的设计亮点在于:

  • 类型安全:通过模板参数指定数据接口类型和返回值类型,确保处理函数签名正确。
  • 动态注册:可以在运行时动态添加、移除操作,无需修改原有代码。
  • 批量处理:支持对一组数据对象批量执行同一操作。
  • 便捷访问:重载了operator[],允许像数组一样通过操作名获取处理函数并调用。

为了实现这些功能,它设计了几个关键组件:

  • vistor类模板:接受两个模板参数DATA_ITFC(数据接口类型)和RET(返回值类型)。内部通过类型萃取得到数据项的真实类型dataItem_t
  • func_t类型别名std::function<RET(dataItem_t&)>,定义了处理函数的签名。
  • funcTan_t类型别名std::unordered_map<std::string, func_t>,用于存储操作名到处理函数的映射。
  • 核心方法addMethoderaseMethodcallcallEachhas以及operator[],提供了完整的操作管理接口。

2.2 类型萃取与静态检查

using dataItem_t = typename std::remove_pointer< typename std::decay< DATA_ITFC >::type >::type;

这里通过std::decay去除引用和CV限定,再通过std::remove_pointer去除指针,最终得到数据项的真实类型。这意味着无论用户传入的是MyDataMyData*还是MyData&,内部都能统一处理为MyData类型。这种设计大大提高了灵活性,用户无需关心指针或引用的修饰。

2.3 核心数据结构

funcTan_t    m_funcs__;

这是一个从std::string(操作名)到func_t(处理函数)的映射表。所有注册的操作都存储在这里,通过操作名可以快速定位到对应的处理逻辑。

2.4 主要方法解析

添加操作方法:addMethod
bool addMethod( const std::string& name , func_t func ) {
    auto rst = m_funcs__.insert( std::make_pair( name , func ) );
    return rst.second;
}
  • 向映射表中插入新的操作名和对应的函数。
  • 如果操作名已存在,插入失败,返回false;否则成功插入,返回true
移除操作方法:eraseMethod
bool eraseMethod( const std::string& name ) {
    auto it = m_funcs__.find( name );
    if( it != m_funcs__.end() ) {
        m_funcs__.erase( it );
        return true;
    }
    return false;
}
  • 查找指定操作名,如果存在则删除并返回true;否则返回false
检查操作方法:has
bool has( const std::string& name ) {
    auto it = m_funcs__.find( name );
    return it != m_funcs__.end();
}
  • 判断指定操作名是否已注册。
调用方法处理单个数据:call
RET call( const std::string& name, dataItem_t& data ) {
    auto it = m_funcs__.find( name );
    if( it != m_funcs__.end() ) {
        return it->second( data );
    }
    return {};
}
  • 根据操作名查找处理函数,如果找到则调用它并返回结果;如果未找到,返回默认构造的RET对象(对于某些类型可能不是期望的,用户需注意)。
调用方法处理批量数据:callEach
template< typename InputIterator >
void callEach( const std::string& name , InputIterator begin , InputIterator end ) {
    auto it = m_funcs__.find( name );
    if( it == m_funcs__.end() ) { 
        throw std::runtime_error( "指定索引的方法不存在" );
    }
    for( auto it1 = begin; it1 != end; ++it1 ) {
        it->second( *it1 );
    }
}
  • 接受一个迭代器范围,对范围内的每个元素调用指定操作名的处理函数。
  • 如果操作名不存在,抛出std::runtime_error异常。
便捷访问操作符:operator[]
func_t operator[]( const std::string& name ) {
    auto it = m_funcs__.find( name );	
    if( it != m_funcs__.end() ) {
        return it->second;
    }
    throw std::runtime_error( "指定索引的方法不存在" );
}
  • 返回指定操作名的处理函数副本,允许用户像这样调用:visitor["draw"](data)
  • 如果操作名不存在,抛出异常。

这种设计让使用体验非常自然,几乎就像是在使用一个函数表。

3. 使用示例

假设我们有一个图形系统,包含多种形状(如圆形、矩形),我们需要对它们执行不同的操作(如计算面积、绘制)。我们可以用访问者模式来组织这些操作。

3.1 定义数据接口和具体类

首先定义一个形状基类(接口),所有形状都继承自它:

#include <iostream>
#include <string>
#include "visitor.hpp"  // 假设头文件名为 visitor.hpp

class Shape {
public:
    virtual ~Shape() {}
    virtual std::string name() const = 0;
};

class Circle : public Shape {
    double radius_;
public:
    Circle(double r) : radius_(r) {}
    double radius() const { return radius_; }
    std::string name() const override { return "Circle"; }
};

class Rectangle : public Shape {
    double width_, height_;
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    double width() const { return width_; }
    double height() const { return height_; }
    std::string name() const override { return "Rectangle"; }
};

3.2 创建访问者并注册操作

我们希望访问者能处理Shape对象,操作返回void(也可以返回其他类型)。注意访问者模板参数是数据接口类型(这里用Shape)和返回值类型。

int main() {
    // 创建访问者,处理 Shape 对象,操作返回 void
    wheels::dm::vistor<Shape, void> visitor;

    // 注册“打印”操作:打印形状信息
    visitor.addMethod("print", [](Shape& s) {
        std::cout << "打印形状: " << s.name() << std::endl;
    });

    // 注册“计算面积”操作:根据具体类型计算面积(需要类型转换)
    visitor.addMethod("area", [](Shape& s) -> double {
        if (auto c = dynamic_cast<Circle*>(&s)) {
            return 3.14159 * c->radius() * c->radius();
        } else if (auto r = dynamic_cast<Rectangle*>(&s)) {
            return r->width() * r->height();
        }
        return 0.0;
    });

    // 创建一些形状对象
    Circle c(5.0);
    Rectangle r(4.0, 6.0);

    // 调用“打印”操作
    visitor.call("print", c);
    visitor.call("print", r);

    // 调用“面积”操作并输出结果
    std::cout << "圆形面积: " << visitor.call("area", c) << std::endl;
    std::cout << "矩形面积: " << visitor.call("area", r) << std::endl;

    // 使用 operator[] 方式调用
    auto printFunc = visitor["print"];
    printFunc(c);   // 等价于 visitor.call("print", c)

    return 0;
}

输出:

打印形状: Circle
打印形状: Rectangle
圆形面积: 78.5397
矩形面积: 24
打印形状: Circle

3.3 批量处理

如果有多个形状需要批量打印,可以用callEach

std::vector<Shape*> shapes = { &c, &r };
visitor.callEach("print", shapes.begin(), shapes.end());

4. 类图(Mermaid)

基于

存储

实现接口

vistor

- map<string, func_t> m_funcs__

+ addMethod(name, func) : bool

+ eraseMethod(name) : bool

+ has(name) : bool

+ call(name, data) : RET

+ callEach(name, begin, end) : void

+ operator[](name) : func_t

«typedef»

func_t

std::function~RET(dataItem_t&) : ~

«deduced»

dataItem_t

«abstract»

Shape

+ name() : string

Circle

- radius: double

+ radius() : double

+ name() : string

Rectangle

- width: double

- height: double

+ width() : double

+ height() : double

+ name() : string

说明:vistor类通过模板参数DATA_ITFC推导出dataItem_t,它应该是某个接口类型。用户定义的形状类(如CircleRectangle)需要实现该接口(这里为Shape)。访问者内部存储的func_t函数接受dataItem_t&,因此可以处理任何派生类对象(多态)。

5. 流程图(Mermaid)

5.1 注册操作流程

用户调用 addMethod(name, func)

访问者向 m_funcs__ 插入键值对

键名是否已存在?

插入失败,返回 false

插入成功,返回 true

5.2 调用操作流程(单个数据)

用户调用 call(name, data)

在 m_funcs__ 中查找 name

找到否?

调用对应的 func(data)

返回 func 的返回值

返回默认构造的 RET

5.3 批量调用流程

用户调用 callEach(name, begin, end)

在 m_funcs__ 中查找 name

找到否?

抛出 runtime_error

遍历迭代器范围 [begin, end)

对每个元素调用 func(*it)

遍历结束,返回

6. 总结

6.1 优点

  • 解耦操作与数据结构:操作以函数形式独立注册,不侵入数据类。
  • 动态扩展:运行时可以动态添加、移除操作,灵活性高。
  • 类型安全:模板参数确保处理函数签名与数据类型一致。
  • 使用便捷operator[] 提供了类似函数表的访问方式,代码简洁。
  • 批量处理支持:通过迭代器轻松处理一组数据。

6.2 注意事项

  • 类型擦除的限制:处理函数接受基类引用,内部可能需要dynamic_cast来判断具体类型(如面积计算示例),这会带来运行时开销。如果数据类型层次固定且性能敏感,可考虑其他设计。
  • 返回值处理call方法在找不到操作时返回默认构造的RET,这可能不是预期行为,用户需确保操作存在或自行处理异常。
  • 异常安全operator[]callEach在操作名不存在时会抛出异常,调用者需捕获处理。

6.3 适用场景

  • 语法树遍历:在编译器中对AST节点进行多种操作(如类型检查、代码生成)。
  • 图形处理:对图形对象执行绘制、缩放、导出等操作。
  • 插件系统:允许外部动态注册处理函数,扩展系统功能。
  • 配置处理:根据不同配置项名称调用相应的处理逻辑。

7. 结语

这个访问者模式模板库以极简的代码实现了经典设计模式的现代化封装。通过将操作函数与名称绑定,它提供了一种灵活、可扩展的方式来处理多态对象集合。希望本文能帮助你理解访问者模式及其C++实现,并在实际项目中灵活运用。

如果你有任何疑问或想法,欢迎留言讨论!


参考资料

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐