我的编程空间,编程开发者的网络收藏夹
学习永远不晚

浅谈 C++17 里的 Visitor 模式

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

浅谈 C++17 里的 Visitor 模式

一、Visitor Pattern

访问者模式是一种行为模式,允许任意的分离的访问者能够在管理者控制下访问所管理的元素。访问者不能改变对象的定义(但这并不是强制性的,你可以约定为允许改变)。对管理者而言,它不关心究竟有多少访问者,它只关心一个确定的元素访问顺序(例如对于二叉树来说,你可以提供中序、前序等多种访问顺序)。

1、组成

Visitor 模式包含两个主要的对象Visitable 对象和 Vistor 对象。此外,作为将被操作的对象,在 Visitor 模式中也包含 Visited 对象。

一个 Visitable 对象,即管理者,可能包含一系列形态各异的元素(Visited),它们可能在 Visitable 中具有复杂的结构关系(但也可以是某种单纯的容纳关系,如一个简单的 vector)。Visitable 一般会是一个复杂的容器,负责解释这些关系,并以一种标准的逻辑遍历这些元素。当 Visitable 对这些元素进行遍历时,它会将每个元素提供给 Visitor 令其能够访问该 Visited 元素。

这样一种编程模式就是 Visitor Pattern

2、接口

为了能够观察每个元素,因此实际上必然会有一个约束:所有的可被观察的元素具有共同的基类 Visited。

所有的 Visitors 必须派生于 Visitor 才能提供给 Visitable.accept(visitor&) 接口。


namespace hicc::util {

    struct base_visitor {
        virtual ~base_visitor() {}
    };
    struct base_visitable {
        virtual ~base_visitable() {}
    };

    template<typename Visited, typename ReturnType = void>
    class visitor : public base_visitor {
    public:
        using return_t = ReturnType;
        using visited_t = std::unique_ptr<Visited>;
        virtual return_t visit(visited_t const &visited) = 0;
    };

    template<typename Visited, typename ReturnType = void>
    class visitable : public base_visitable {
    public:
        virtual ~visitable() {}
        using return_t = ReturnType;
        using visitor_t = visitor<Visited, return_t>;
        virtual return_t accept(visitor_t &guest) = 0;
    };

} // namespace hicc::util

3、场景

以一个实例来说,假设我们正在设计一套矢量图编辑器,在画布(Canvas)中,可以有很多图层(Layer),每一图层包含一定的属性(例如填充色,透明度),并且可以有多种图元(Element)。图元可以是 PointLineRectArc 等等。

为了能够将画布绘制在屏幕上,我们可以有一个 Screen 设备对象,它实现了 Visitor 接口,因此画布可以接受 Screen 的访问,从而将画布中的图元绘制到屏幕上。

如果我们提供 Printer 作为观察者 ,那么画布将能够把图元打印出来。

如果我们提供 Document 作为观察者,那么画布将能够把图元特性序列化到一个磁盘文件中去。

如果今后需要其它的行为,我们可以继续增加新的观察者,然后对画布及其所拥有的图元进行类似的操作。

4、特点

  • 如果你需要对一个复杂对象结构 (例如对象树) 中的所有元素执行某些操作, 可使用访问者模式。
  • 访问者模式将非主要的功能从对象管理者身上抽离,所以它也是一种解耦手段。
  • 如果你正在制作一个对象库的类库,那么向外提供一个访问接口,将会有利于用户无侵入地开发自己的 visitor 来访问你的类库——他不必为了自己的一点点事情就给你 issue/pull request
  • 对于结构层级复杂的情况,要善于使用对象嵌套与递归能力,避免反复编写相似逻辑。

请查阅 canvalayergroup 的参考实现,它们通过实现 drawable vistiable<drawable> 的方式完成了嵌套性的自我管理能力,并使得 accept() 能够递归地进入每一个容器中。

5、实现

我们以矢量图编辑器的一部分为示例进行实现,采用了前面给出的基础类模板。

drawable 和 基础图元

首先做 drawable/shape 的基本声明以及基础图元:


namespace hicc::dp::visitor::basic {

  using draw_id = std::size_t;

  
  struct drawable {
    virtual ~drawable() {}
    friend std::ostream &operator<<(std::ostream &os, drawable const *o) {
      return os << '<' << o->type_name() << '#' << o->id() << '>';
    }
    virtual std::string type_name() const = 0;
    draw_id id() const { return _id; }
    void id(draw_id id_) { _id = id_; }

    private:
    draw_id _id;
  };

  #define MAKE_DRAWABLE(T)                                            \
    T(draw_id id_) { id(id_); }                                     \
    T() {}                                                          \
    virtual ~T() {}                                                 \
    std::string type_name() const override {                        \
        return std::string{hicc::debug::type_name<T>()};            \
    }                                                               \
    friend std::ostream &operator<<(std::ostream &os, T const &o) { \
        return os << '<' << o.type_name() << '#' << o.id() << '>';  \
    }

  //@formatter:off
  struct point : public drawable {MAKE_DRAWABLE(point)};
  struct line : public drawable {MAKE_DRAWABLE(line)};
  struct rect : public drawable {MAKE_DRAWABLE(rect)};
  struct ellipse : public drawable {MAKE_DRAWABLE(ellipse)};
  struct arc : public drawable {MAKE_DRAWABLE(arc)};
  struct triangle : public drawable {MAKE_DRAWABLE(triangle)};
  struct star : public drawable {MAKE_DRAWABLE(star)};
  struct polygon : public drawable {MAKE_DRAWABLE(polygon)};
  struct text : public drawable {MAKE_DRAWABLE(text)};
  //@formatter:on
  // note: dot, rect (line, rect, ellipse, arc, text), poly (triangle, star, polygon)
}

为了调试目的,我们重载了 '<<' 流输出运算符,而且利用宏 MAKE_DRAWABLE 来削减重复性代码的键击输入。在 MAKE_DRAWABLE 宏中,我们通过 hicc::debug::type_name<T>() 来获得类名,并将此作为字符串从 drawable::type_name() 返回。

出于简化的理由基础图元没有进行层次化,而是平行地派生于 drawable

复合性图元和图层

下面声明 group 对象,这种对象包含一组图元。由于我们想要尽可能多的递归结构,所以图层也被认为是一种一组图元的组合形式:


namespace hicc::dp::visitor::basic {

  struct group : public drawable
    , public hicc::util::visitable<drawable> {
    MAKE_DRAWABLE(group)
      using drawable_t = std::unique_ptr<drawable>;
    using drawables_t = std::unordered_map<draw_id, drawable_t>;
    drawables_t drawables;
    void add(drawable_t &&t) { drawables.emplace(t->id(), std::move(t)); }
    return_t accept(visitor_t &guest) override {
      for (auto const &[did, dr] : drawables) {
        guest.visit(dr);
        UNUSED(did);
      }
    }
  };

  struct layer : public group {
    MAKE_DRAWABLE(layer)
    // more: attrs, ...
  };
}

group class 中已经实现了 visitable 接口,它的 accept 能够接受访问者的访问,此时 图元组 group 会遍历自己的所有图元并提供给访问者。

我们还可以基于 group class 创建 compound 图元类型,它允许将若干图元组合成一个新的图元元件,两者的区别在于,group 一般是 UI 操作中的临时性对象,而 compound 图元能够作为元件库中的一员供用户挑选和使用。
默认时 guest 会访问 visited const & 形式的图元,也就是只读方式。

图层至少具有 group 的全部能力,所以面对访问者它的做法是相同的。图层的属性部分(maskoverlay 等等)被略过了。

画布 Canvas

画布包含了若干图层,所以它同样应该实现 visitable 接口:


namespace hicc::dp::visitor::basic {

  struct canvas : public hicc::util::visitable<drawable> {
    using layer_t = std::unique_ptr<layer>;
    using layers_t = std::unordered_map<draw_id, layer_t>;
    layers_t layers;
    void add(draw_id id) { layers.emplace(id, std::make_unique<layer>(id)); }
    layer_t &get(draw_id id) { return layers[id]; }
    layer_t &operator[](draw_id id) { return layers[id]; }

    virtual return_t accept(visitor_t &guest) override {
      // hicc_debug("[canva] visiting for: %s", to_string(guest).c_str());
      for (auto const &[lid, ly] : layers) {
        ly->accept(guest);
      }
      return;
    }
  };
}

其中,add 将会以默认参数创建一个新图层,图层顺序遵循向上叠加方式。get 和 [] 运算符能够通过正整数下标访问某一个图层。但是代码中没有包含图层顺序的管理功能,如果有意,你可以添加一个 std::vector<draw_id> 的辅助结构来帮助管理图层顺序。

现在我们来回顾画布-图层-图元体系,accept 接口成功地贯穿了整个体系。

是时候建立访问者们了

screen 或 printer

这两者实现了简单的访问者接口:


namespace hicc::dp::visitor::basic {
  struct screen : public hicc::util::visitor<drawable> {
    return_t visit(visited_t const &visited) override {
      hicc_debug("[screen][draw] for: %s", to_string(visited.get()).c_str());
    }
    friend std::ostream &operator<<(std::ostream &os, screen const &) {
      return os << "[screen] ";
    }
  };

  struct printer : public hicc::util::visitor<drawable> {
    return_t visit(visited_t const &visited) override {
      hicc_debug("[printer][draw] for: %s", to_string(visited.get()).c_str());
    }
    friend std::ostream &operator<<(std::ostream &os, printer const &) {
      return os << "[printer] ";
    }
  };
}

hicc::to_string 是一个简易的串流包装,它做如下的核心逻辑:


template<typename T>
inline std::string to_string(T const &t) {
  std::stringstream ss;
  ss << t;
  return ss.str();
}

test case

测试程序构造了微型的画布以及几个图元,然后示意性地访问它们:


void test_visitor_basic() {
    using namespace hicc::dp::visitor::basic;

    canvas c;
    static draw_id id = 0, did = 0;
    c.add(++id); // added one graph-layer
    c[1]->add(std::make_unique<line>(++did));
    c[1]->add(std::make_unique<line>(++did));
    c[1]->add(std::make_unique<rect>(++did));

    screen scr;
    c.accept(scr);
}

输出结果应该类似于这样:


--- BEGIN OF test_visitor_basic                       ----------------------
09/14/21 00:33:31 [debug]: [screen][draw] for: <hicc::dp::visitor::basic::rect#3>
09/14/21 00:33:31 [debug]: [screen][draw] for: <hicc::dp::visitor::basic::line#2>
09/14/21 00:33:31 [debug]: [screen][draw] for: <hicc::dp::visitor::basic::line#1
--- END OF test_visitor_basic                         ----------------------

It took 2.813.753ms

二、Epilogue

Visitor 模式有时候能够被迭代器模式所代替。但是迭代器常常会有一个致命缺陷而影响了其实用性:迭代器本身可能是僵化的、高代价的、效率低下的——除非你做出了最恰当的设计时选择并实现了最精巧的迭代器。 它们两者都允许用户无侵入地访问一个已知的复杂容器的内容。

到此这篇关于浅谈 C++17 里的 Visitor 模式的文章就介绍到这了,更多相关 C++17 里的 Visitor 模式内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

浅谈 C++17 里的 Visitor 模式

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

浅谈vim的四种模式及模式切换

vim和记事本或WORD不一样,不是一打开后就可以输入文字,此时它处于正常模式。 vim一共有4个模式: 正常模式 (Normal-mode) 插入模式 (Insert-mode) 命令模式 (Command-mode) 可视模式 (V
2022-06-04

浅谈对于DAO设计模式的理解

为了降低耦合性,提出了DAO封装数据库操作的设计模式。它可以实现业务逻辑与数据库访问相分离。相对来说,数据库是比较稳定的,其中DAO组件依赖于数据库系统,提供数据库访问的接口。一般的DAO的封装由以下另个原则: · 一个表对应一个表,相应地
2023-05-31

浅谈达梦数据库的兼容模式

达梦实现了很多当前主流数据库oracle、mysq、sqlserver的功能特性,在兼容性方面做得很不错。开发人员从上述3种数据库切换到达梦数据库还是很轻松的,我们来看一下达梦数据库的兼容模式。         先进入控制台工具: 选中一个实例: 找到兼容
浅谈达梦数据库的兼容模式
2016-07-31

浅谈一下Java的双亲委派模式

这篇文章主要介绍了一下Java的双亲委派模式,双亲委派指得是,JVM在加载类时,会委派给ExtClassLoader和BootstrapClassLoader进⾏加载,如果没加载到才由⾃⼰进⾏加载,需要的朋友可以参考下
2023-05-18

C++中的策略模式浅析

策略模式属于C++设计模式中行为模式之一,该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。本文将通过示例详细讲解这一模式,需要的可以参考一下
2023-02-27

浅谈 Android 7.0 多窗口分屏模式的实现

从 Android 7.0 开始,Google 推出了一个名为“多窗口模式”的新功能,也就是我们常说的“分屏模式”。那么,这个功能有什么用呢?作为开发者,我们又能做些什么? Android 7.0 添加了对同时显示多个 APP 的支持。在手
2022-06-06

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录