sbi*_*sbi 7 c++ visitor visitor-pattern
我们正在开发的软件系统需要在组件之间交换大量数据。数据的结构我们称之为变量树。这些数据本质上是组件之间的接口。代表特定接口的 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
Run Code Online (Sandbox Code Playgroud)
因此,用于遍历这些树中节点的访问者基类具有接受所有整数类型(有符号和无符号)、所有浮点类型、布尔值和字符串的函数,无论是常量节点还是非常量节点。(我们目前计划将 enum 类型映射到 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
};
Run Code Online (Sandbox Code Playgroud)
为了能够实际处理这个,我们使用 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.
};
Run Code Online (Sandbox Code Playgroud)
这使得处理某些类型的节点变得可以忍受:
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 */}
};
Run Code Online (Sandbox Code Playgroud)
在大量涉及电气工程的应用领域开始一个新的 C++ 软件组件,我们决定采用编译时检查单元库(又名“维度分析库”)。单元库很棒,因为它们使用类型系统在编译时检查代码的正确性。他们通过创建几乎无限数量的类型来实现这一点,这些类型不仅对底层的内置类型(int、double、...)进行编码,而且对物理单位(质量、能量)、尺度(毫、兆、 -) 和一些标签(有功/无功/视在功率,开尔文/摄氏度)。
可以从接口描述中轻松生成具有正确物理单位的树节点。但是如果单元类型的值存储在树节点中,这将使我们的访问者基类需要数千个重载来接受使用的所有不同节点类型,提示开发人员在节点需要以前未使用的单元时添加新的单元,或者在一个以前没有使用过的规模。我们可以想出一些巧妙的模板滥用来在编译时从类型列表生成所有这些虚函数。然而,当产生的虚拟表大小是否会成为问题的问题出现时,我开始怀疑我们当前的方法是否真的仍然是解决问题的最佳方法。
常识说,如果你有很多节点类型和很少的算法来遍历它们,你应该在节点本身中使用虚函数。如果,OTOH,你有很多算法和很少的节点类型,你应该使用访问者模式。常识还说,如果两者兼而有之,那你就完蛋了。
我的感觉是,到目前为止,我们几乎无法适应少节点类型/多算法的抽屉。随着编译时单元的类型扩散,我的胆量说我们非常适合多个抽屉。简而言之:我们可能会被搞砸。
在去年年底之前一直与 C++03 紧密相连(嵌入式世界进展缓慢),我们当然不太熟悉其他 C++ 程序员已经使用了近十年的许多工具。所以我希望我们在这里错过了一个明显的解决方案。
或者也许我们遗漏了一些不那么明显的东西?
以防万一有人在多年后遇到类似情况时发现这一点并想知道我们做了什么:
使用单位数量的节点现在源自其底层 POD 的节点(通常为double)。这些节点还提供虚拟函数来确定enum数量的单位(an)和规模(毫、千)。对于纯 POD 节点,这些默认设置为“无单位”和“无比例”。从它们派生的存储数量的节点会覆盖那些节点以返回适当的数据。
因此,它是通过双重分派(不能将整数与浮点混淆)的编译时安全性和运行时需要完成的检查(这是否附加了一个单元?)的混合体,这防止了重载数量爆炸成千。
这意味着我们可以使用的单元数量目前受到编译时列出所有单元的枚举的限制,但我们发现这在实践中是可以接受的,目前我们仅使用大约 2 打单元。不过,如果这成为一个问题,我们可以通过运行时单元类型对其进行建模,使用代表基本单元的幂列表,就像编译时单元类型一样。