如果您有很多节点类型,则为访问者模式

问题描述

我们有什么?

我们正在开发的软件系统需要在组件之间交换大量数据。数据是在我们称为变量树的结构中构建的。这些数据实质上是组件之间的接口。代表特定接口的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

因此,用于遍历那些树中节点的访问者基类具有接受const和非const节点的所有整数类型(有符号和无符号),所有浮点类型,布尔值和字符串的功能。 (我们目前计划将枚举类型映射为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 ++程序员已经使用了近十年的许多工具不太熟悉。所以我希望我们在这里找不到一个明显的解决方案。

或者我们可能缺少了一些不太明显的东西?

解决方法

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

使用单位数量的节点现在源自其底层 POD 的节点(通常为 double)。这些节点还提供虚拟函数来确定数量的单位(enum)和规模(毫、千克)。对于纯 POD 节点,这些默认为“无单位”和“无规模”。从它们派生的节点存储数量会覆盖这些节点以返回适当的数据。

所以它是通过双重分派(不能将整数与浮点数混淆)和需要在运行时完成的检查(是否附加了单元?)从爆炸到数千的重载。

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