C++访问者模式及其实现(非常详细)
访问者模式(Visitor Pattern)在不修改已有对象结构的前提下定义新的操作,也就是在不改变数据的结构的情况下能够自由地添加或者移除操作。该模式能够将数据结构和操作解耦,使操作可以独立变化。
访问者模式的核心思想是将操作封装在访问者对象中,而不是封装在元素对象中。元素对象通过接受访问者对象的访问,将自身信息传递给访问者对象,从而完成操作。这种方式可以在不修改元素对象的前提下,增加新的操作,具有较好的扩展性。
使用访问者模式的一个典型场景是处理对象结构中的各个对象,但根据不同的访问者对象,操作会有所不同。
访问者模式增加新的操作非常方便,只需创建新的具体访问者类;将相关操作集中到访问者对象中,使元素对象和操作解耦,提高了系统的灵活性。可以对对象结构进行多种不同的操作,而无须修改元素对象结构。

图 1 传统访问者模式UML简图

图 2 C++11模板访问者模式UML简图
模板类 visitor 是定义的基础模板接口,实际实现时可以使用特化或者继承方式实现 concreteVisitor 类作为业务中使用的类。DATA_ITFC 是作为模板参数传递给 visitor 的数据访问接口定义。实际数据的访问接口需要延迟到业务中实现。
访问者模式 visitor 模板类中维护了一个函数对象表,该对象表可以使用 std::string 进行检索获取,代码如下:
addMethod() 方法用来增加新的处理方法;函数 eraseMethod() 用来移除数据处理方法。利用 std::unordered_set 和函数对象配合,增删处理方法就可以像增删普通数据一样方便,代码如下:
对下标运算符重载提供了更加简便的调用方法的方式,例如可以使用 ["abc"](xxx) 的方式调用已经添加的方法,代码如下:
函数 call() 用来指定方法索引调用一个明确的方法来处理一条数据内容。函数 callEach() 则可以指定数据范围调用一个明确的方法进行处理,代码如下:
在 main() 函数中首先通过 addMethod() 方法添加处理函数,然后通过下标运算符调用对应的处理函数。也可以通过 callEach() 方法访问指定范围的数据内容,代码如下:
访问者模式的核心思想是将操作封装在访问者对象中,而不是封装在元素对象中。元素对象通过接受访问者对象的访问,将自身信息传递给访问者对象,从而完成操作。这种方式可以在不修改元素对象的前提下,增加新的操作,具有较好的扩展性。
使用访问者模式的一个典型场景是处理对象结构中的各个对象,但根据不同的访问者对象,操作会有所不同。
访问者模式增加新的操作非常方便,只需创建新的具体访问者类;将相关操作集中到访问者对象中,使元素对象和操作解耦,提高了系统的灵活性。可以对对象结构进行多种不同的操作,而无须修改元素对象结构。
传统访问者模式
传统访问者模式由几个核心组件组成:元素(Element)、具体元素(Concrete Element)、访问者(Visitor)、具体访问者(Concrete Visitor)和对象结构(Object Structure),如下图所示。
图 1 传统访问者模式UML简图
- 元素定义了一个接受访问者对象的操作接口,通常包含一个 accept() 方法,用于接受访问者对象的访问;
- 具体元素实现了元素接口,提供了具体的实现逻辑;
- 访问者定义了访问元素对象的操作接口,通常包含多个重载方法,每种方法用于访问不同类型的元素对象;
- 具体访问者实现了访问者接口,提供了具体的操作逻辑,每种方法实现对不同类型元素对象进行处理。对象结构包含一组元素对象,提供了遍历元素对象的方法,以及与访问者的交互方法。
C++11元编程下的访问者模式
使用 C++11 元编程技术可以使用模板参数将数据和方法独立出来。利用函数对象可以方便灵活地增加和移除数据处理方法,如下图所示。
图 2 C++11模板访问者模式UML简图
模板类 visitor 是定义的基础模板接口,实际实现时可以使用特化或者继承方式实现 concreteVisitor 类作为业务中使用的类。DATA_ITFC 是作为模板参数传递给 visitor 的数据访问接口定义。实际数据的访问接口需要延迟到业务中实现。
C++访问者模式实现和解析
模板类 visitor 的两个模板参数 DATA_T 和 RET 分别是数据对象类型和访问者函数的返回值类型,其基本原理是利用容器和函数对象实现对处理方法进行存储管理,从而实现针对数据的增删处理方法。访问者模式和解释器模式配合起来可以实现非常灵活的处理系统。访问者模式 visitor 模板类中维护了一个函数对象表,该对象表可以使用 std::string 进行检索获取,代码如下:
template< typename DATA_ITFC,typename RET > class visitor { public: using dataItem_t = typename std::remove_pointer< typename std::decay< DATA_ITFC >::type >::type; using func_t = std::function< RET(dataItem_t&) >; //函数对象容器类型,使用了std::unordered_map来记录。这种方式有很高 //的访问速度 using funcTan_t = typename std::unordered_map< std::string,func_t >; protected: funcTan_t m_funcs__; public: visitor(){} virtual~visitor(){}
addMethod() 方法用来增加新的处理方法;函数 eraseMethod() 用来移除数据处理方法。利用 std::unordered_set 和函数对象配合,增删处理方法就可以像增删普通数据一样方便,代码如下:
bool addMethod(const std::string&name,func_t func) { auto rst = m_funcs__.insert(std::make_pair(name,func)); return rst.second; } bool eraseMethod(const std::string&name) { auto it = m_funcs__.find(name); if(it){ m_funcs__.erase(it); return true; } return false; }
对下标运算符重载提供了更加简便的调用方法的方式,例如可以使用 ["abc"](xxx) 的方式调用已经添加的方法,代码如下:
func_t operator[](const std::string&name){ auto it = m_funcs__.find(name); if(it){ return it->second; } return{}; }
函数 call() 用来指定方法索引调用一个明确的方法来处理一条数据内容。函数 callEach() 则可以指定数据范围调用一个明确的方法进行处理,代码如下:
RET call(const std::string&name,dataItem_t&data) { auto it = m_funcs__.find(name); if(it){ return it->second(data); } return{}; } template< typename InputIterator > void callEach(const std::string&name, InputIterator begin,InputIterator end) { auto it = m_funcs__.find(name); for(auto it1 = begin;it1! = end; ++it1){ it->second(* it1); } } //判断是否存在指定索引的方法 bool has(const std::string&name) { auto it = m_funcs__.find(name); return it! = m_funcs__.end(); } };
C++访问者模式应用示例
在下面的示例中结构体作为被访问的数据类。函数 func1() 和 func2() 分别是两个具体访问的处理函数。在 main() 函数中首先通过 addMethod() 方法添加处理函数,然后通过下标运算符调用对应的处理函数。也可以通过 callEach() 方法访问指定范围的数据内容,代码如下:
#include <iostream> #include <string> #include "designM/vistor.hpp" // 自定义数据类型 struct Data { Data(int value) : value(value) {} int value; }; // 处理函数1 int func1(Data& data) { std::cout << "func1 called with value: " << data.value << std::endl; return data.value + 1; } // 处理函数2 int func2(Data& data) { std::cout << "func2 called with value: " << data.value << std::endl; return data.value * 2; } int main() { // 创建 visitor 对象,数据类型为 Data,返回类型为 int visitor<Data, int> myVisitor; // 将方法添加到 visitor 对象 myVisitor.addMethod("func1", func1); myVisitor.addMethod("func2", func2); // 创建数据对象 Data myData(10); // 调用指定名称的方法处理数据 int result1 = myVisitor["func1"](myData); std::cout << "Result1: " << result1 << std::endl; int result2 = myVisitor["func2"](myData); std::cout << "Result2: " << result2 << std::endl; // 批量处理数据 std::vector<Data> dataVec = {Data(20), Data(30)}; myVisitor.callEach("func1", dataVec.begin(), dataVec.end()); return 0; }程序的运行结果如下:
func1 called with value:10
Result1:11
func2 called with value:10
Result2:20
func1 called with value:20
func1 called with value:30