访客模式(如果节点类型非常多)

Visitor pattern if you have very many node types

提问人:sbi 提问时间:10/3/2020 最后编辑:sbi 更新时间:6/5/2021 访问量:268

问:

我们有什么?

我们正在开发的软件系统需要在组件之间交换大量数据。数据以我们所说的变量树为结构。这些数据实质上是组件之间的接口。表示特定接口的 C++ 代码是从接口描述自动生成的。执行实际数据交换有不同的底层实现,例如 OPC/UA,但大多数代码都不受此限制。重要的节点类型是存储值和值数组的节点类型,这两种类型几乎可以针对任何类型进行实例化。

class node { /* whatever all nodes have in common */ };

class value_node : public node { /* polymorphic access to value */ };

template<typename T>
class typed_value_node : public value_node { /* type-safe access to value */ };

// imagine pretty much the same for array_node and typed_array_node

因此,用于遍历这些树中的节点的访问者基类具有接受所有整数类型(有符号和无符号)、所有浮动类型、布尔值和字符串的函数,包括常量和非常量节点。(我们目前计划将枚举类型映射到 int/string 对,但关于这一点还没有确定。所有这些重载都存在于值和数组中。
目前,大约有 70 个重载:

class visitor {
public:
    virtual ~visitor() = default;

    virtual void accept(      typed_value_node<         char     >&)  = 0;
    virtual void accept(const typed_value_node<         char     >&)  = 0;


    virtual void accept(      typed_value_node<  signed char     >&)  = 0;
    virtual void accept(const typed_value_node<  signed char     >&)  = 0;

    ...

    virtual void accept(      typed_value_node<  signed long long>&)  = 0;
    virtual void accept(const typed_value_node<  signed long long>&)  = 0;


    virtual void accept(      typed_value_node<unsigned char     >&)  = 0;
    virtual void accept(const typed_value_node<unsigned char     >&)  = 0;

    ...

    virtual void accept(      typed_value_node<unsigned long long>&)  = 0;
    virtual void accept(const typed_value_node<unsigned long long>&)  = 0;


    virtual void accept(      typed_value_node<bool              >&)  = 0;
    virtual void accept(const typed_value_node<bool              >&)  = 0;
    ...


    // repeat for typed_array_node
};

为了能够实际处理这个问题,我们使用 CRTP 来制作一个访问者实现调用派生类的函数模板:

template<typename Derived>
class visitor_impl : public visitor {
public:
    void accept(      typed_value_node<char>& node) override
    {static_cast<Derived*>(this)->do_visit(node);}
    void accept(const typed_value_node<char>& node) override
    {static_cast<Derived*>(this)->do_visit(node);}

    // etc.
};

这使得只处理某种节点是可以忍受的:

class my_value_node_visitor : public visitor_impl<my_value_node_visitor> {
public:
    template<typename T>
    void accept(const typed_value_node<T>&) {/* I wanna see these*/}

    template<typename T>
    void accept(const T&)                   {/* I don't care about those */}
};

我们想要什么?

在电子工程密集的应用领域开始一个新的 C++ 软件组件,我们决定使用编译时检查单元库(又名“维度分析库”)。单元库的优点在于它们使用类型系统在编译时检查代码的正确性。他们通过创建几乎不受限制的类型数量来实现这一点,这些类型不仅编码底层内置类型(int、double,...),还编码物理单位(质量、能量)、刻度(毫、兆)和一些标签(有功/无功/视在功率,开尔文/摄氏度)。

怎么了?

从接口描述中可以很容易地生成具有正确物理单元的树节点。但是,如果单位类型的值存储在树节点中,这将使我们的访问者基类需要数千个重载来接受使用的所有不同节点类型,从而促使开发人员在节点需要以前未使用过的单位或以前未使用的比例中添加新单位。我们可以想出一些巧妙的模板滥用方法,在编译时从类型列表生成所有这些虚函数。然而,当问题出现时,由此产生的虚拟表大小是否会成为一个问题,我开始怀疑我们目前的方法是否真的仍然是解决这个问题的最佳方法。

常识说,如果你有很多节点类型,而很少有算法来遍历它们,你应该在节点本身中使用虚函数。如果 OTOH 有很多算法很少的节点类型,则应使用访客模式。常识还说,如果你同时拥有很多,你就完蛋了。

我的感觉是,到目前为止,我们几乎无法适应少数节点类型/多算法的抽屉。随着编译时单元带来的类型激增,我的直觉表明,我们变得非常适合任一抽屉。简而言之:我们可能被搞砸了。

我们现在该怎么办?

直到去年年底(嵌入式世界发展缓慢),我们一直与C++03紧密联系在一起,我们当然不太熟悉其他C++程序员已经使用了近十年的许多工具。所以我希望我们在这里缺少一个明显的解决方案。

或者也许我们错过了一些不那么明显的东西?

C++ 访客模式

评论

0赞 wcochran 10/3/2020
使用访问者模式似乎是一个不错的选择,许多算法 + 类型是有问题的。满足“开闭原则”(避免修改已建立的代码,但允许扩展到新类型)的一个好方法是使用“双重调度”。重构.guru/design-patterns/visitor-double-dispatch这是为编译器实现语法树的常用技巧,编译器也必须处理数以百万计的类型。
0赞 user253751 10/3/2020
@wcochran 这是双重调度。问题是如何理智地调度几百种类型。
0赞 wcochran 10/3/2020
@user253751好的。使用组合某些类型(可以与 )有用吗?std::variantstd::visit
2赞 Sopel 10/4/2020
1. 也许将一些类型组合并为更通用的类型。例如,所有有符号整数类型都设置为 long long,所有浮点类型为 double,等等。2. 对于单元库,也许这是考虑运行时库的正确位置?-- 这里问题的解决方案最终将是找到正确的子集,从编译时转移到运行时。
0赞 parktomatomi 10/4/2020
那么,这是否意味着每种类型都有 15 种不同的变体,具有不同的 SI 前缀,每种变体都有自己的访客条目?哎呀。这似乎是一个简化的好目标 - 它们之间是否有任何相互作用不仅仅是“乘以使它们达到相同的规模”?在那之后,数字类型可能是一个很好的目标 - 是否有任何交互,没有“将一个投射到更精确的类型”?

答:

0赞 sbi 6/5/2021 #1

以防万一有人在多年后遇到类似情况时发现这一点,并想知道我们做了什么:

使用单位数量的节点现在派生自其基础 POD 的节点(通常 )。这些节点还提供虚拟函数来确定数量的单位 (an) 和比例 (milli, kilo)。对于纯 POD 节点,这些默认为“无单位”和“无缩放”。从它们派生的节点存储数量会覆盖这些节点,以返回相应的数据。doubleenum

因此,它混合了通过双重调度(不能将整数与浮点混淆)和需要在运行时完成的检查(这是否附加了单元?)来确保编译时安全性,从而防止重载数量激增到数千个。

这意味着我们目前可以使用的单元数量受到一个枚举的限制,该枚举在编译时列出了所有单元,但我们发现这在实践中是可以接受的,我们目前只使用了大约 2 打单元。但是,如果这成为一个问题,我们可以通过运行时单元类型对其进行建模,使用表示基本单元的幂列表,就像编译时单元类型一样。