C++:此模式是否有名称,是否可以改进

C++: Does this pattern have a name, and can it be improved?

本文关键字:是否 有名称 模式 C++      更新时间:2023-10-16

动机

假设我正在编写一个Tree类。我将用一个Tree::Node类来表示树的节点。类的方法可能会返回Tree::Node对象并将它们作为参数,例如获取节点父级的方法:Node getParent(Node)

我还想要一个SpecialTree课。SpecialTree应该扩展Tree的接口,并且可以在Tree所在的任何地方使用。

在幕后,TreeSpecialTree可能具有完全不同的实现。例如,我可能会使用库的GraphA类来实现Tree,以便Tree::NodeGraphA::Node的精简包装器或typedef。另一方面,SpecialTree可能是根据GraphB对象实现的,而Tree::Node包装GraphB::Node

稍后我将提供处理树的函数,例如深度优先搜索函数。此函数应交替接受TreeSpecialTree对象。

模式

我将使用模板化接口类来定义树和特殊树的接口。模板参数将是实现类。例如:

template <typename Implementation>
class TreeInterface
{
public:
typedef typename Implementation::Node Node;
virtual Node addNode() = 0;
virtual Node getParent(Node) = 0;
};
class TreeImplementation
{
GraphA graph;   
public:
typedef GraphA::Node Node;
Node addNode() { return graph.addNode(); }
Node getParent() { // ...return the parent... }
};
class Tree : public TreeInterface<TreeImplementation>
{
TreeImplementation* impl;
public:
Tree() : impl(new TreeImplementation);
~Tree() { delete impl; }
virtual Node addNode() { return impl->addNode(); }
virtual Node getParent() { return impl->getParent(); }
};

然后我可以从TreeInterface中得出SpecialTreeInterface

template <typename Implementation>
class SpecialTreeInterface : public TreeInterface<Implementation>
{
virtual void specialTreeFunction() = 0;
};

并定义SpecialTreeSpecialTreeImplementation类似于TreeTreeImplementation

我的深度优先搜索函数可能如下所示:

template <typename T>
void depthFirstSearch(TreeInterface<T>& tree);

由于SpecialTree派生自TreeInterface,这将适用于Tree对象和SpecialTree对象。

选择

另一种方法是更多地依赖模板,以便SpecialTree根本不是类型层次结构中TreeInterface的后代。在这种情况下,我的DFS函数将看起来像template <typename T> depthFirstSearch(T& tree)。这也抛弃了严格定义的接口,该接口准确描述了Tree或其后代应该具有的方法。由于SpecialTree应该始终Tree一样,但提供了一些额外的方法,我喜欢使用接口。

与其将TreeInterface模板参数作为实现,我可以让它采用一个"表示"类来定义Node的外观(它还必须定义Arc的外观,等等)。但是由于我可能需要每个实现中的一个,我想我想把它与实现类本身放在一起。

使用此模式可以获得什么?大多数情况下,松散的耦合。如果我想更改Tree背后的实现,SpecialTree一点也不介意,因为它只继承接口。

问题

那么,这种模式有名字吗?我通过在ContourTree中存储指向ContourTreeImplementation的指针来使用句柄主体模式。但是拥有模板化界面的方法呢?这有名字吗?

有没有更好的方法可以做到这一点?看起来我确实在重复自己,写了很多样板代码,但那些嵌套Node类给我带来了麻烦。如果Tree::NodeSpecialTree::Node有相当相似的实现,我可以在TreeInterface中为Node定义一个NodeInterface接口,并在TreeSpecialTree中覆盖节点类的实现。但事实上,我不能保证这是真的。Tree::Node可以包装一个GraphA::NodeSpecialTree::Node可以包装一个整数。所以这种方法不太有效,但似乎仍有改进的余地。有什么想法吗?

看起来像是奇怪重复出现的模板模式和 Pimpl 习语的混合体。

在CRTP中,我们从TreeInterface<Tree>派生Tree;在你的代码中,你从TreeInterface<TreeImplementation>派生Tree。所以这也正如@ElliottFrisch所说:这是战略模式的应用。代码的某些部分关心Tree符合TreeInterface,而其他某些部分关心它使用特定策略TreeImplementation的事实。

有没有更好的方法可以做到这一点?看来我确实在重复自己很多

嗯,这取决于您的运行时要求是什么。当我查看您的代码时,我突然想到的是您正在使用virtual方法 - 啧啧!您的类层次结构如下所示:

Tree is a child of
TreeInterface<TreeImplementation>
SpecialTree is a child of
TreeInterface<SpecialTreeImplementation>

请注意,TreeInterface<X>::addNode()恰好是virtual的事实与TreeInterface<Y>::addNode()是否是虚拟的完全无关! 因此,将这些方法设为virtual不会为我们带来任何运行时多态性;我不能写一个接受任意TreeInterfaceBase实例的函数,因为我们没有一个TreeInterfaceBase。我们所拥有的只是一袋不相关的基类TreeInterface<T>

那么,为什么存在这些virtual方法呢?啊哈。您正在使用virtual将信息从派生类传递回父类:子级可以通过继承"看到"其父级,父级可以通过virtual"看到"子级。这是通常通过CRTP解决的问题。

因此,如果我们使用 CRTP(因此不再需要virtual的东西),我们将只有这个:

template <typename Parent>
struct TreeInterface {
using Node = typename Parent::Node;
Node addNode() { return static_cast<Parent*>(this)->addNode(); }
Node getParent(Node n) const { return static_cast<Parent*>(this)->getParent(n); }
};
struct ATree : public TreeInterface<ATree> {
GraphA graph;
typedef GraphA::Node Node;
Node addNode() { return graph.addNode(); }
Node getParent(Node n) const { // ...return the parent... }
};
struct BTree : public TreeInterface<BTree> {
GraphB graph;
typedef GraphB::Node Node;
Node addNode() { return graph.addNode(); }
Node getParent(Node n) const { // ...return the parent... }
};
template <typename Implementation>
void depthFirstSearch(TreeInterface<Implementation>& tree);

在这一点上,有人可能会说我们根本不需要丑陋的指针投射CRTP,我们可以写

struct ATree {
GraphA graph;
typedef GraphA::Node Node;
Node addNode() { return graph.addNode(); }
Node getParent(Node n) const { // ...return the parent... }
};
struct BTree {
GraphB graph;
typedef GraphB::Node Node;
Node addNode() { return graph.addNode(); }
Node getParent(Node n) const { // ...return the parent... }
};
template <typename Tree>
void depthFirstSearch(Tree& tree);

我个人会同意他们的看法。

好的,您担心的是,无法通过类型系统确保调用方传递给depthFirstSearchT实际上符合TreeInterface。好吧,我认为执行该限制的最C++11式的方式是static_assert。例如:

template<typename Tree>
constexpr bool conforms_to_TreeInterface() {
using Node = typename Tree::Node;  // we'd better have a Node typedef
static_assert(std::is_same<decltype(std::declval<Tree>().addNode()), Node>::value, "addNode() has the wrong type");
static_assert(std::is_same<decltype(std::declval<Tree>().getParent(std::declval<Node>())), Node>::value, "getParent() has the wrong type");
return true;
}
template <typename T>
void depthFirstSearch(T& tree)
{
static_assert(conforms_to_TreeInterface<T>(), "T must conform to our defined TreeInterface");
...
}

请注意,如果不符合T,我的conforms_to_TreeInterface<T>()实际上将静态断言失败;它永远不会真正返回false。您同样可以让它返回truefalse,然后在depthFirstSearch()中击中static_assert

无论如何,这就是我解决问题的方式。请注意,我的整篇文章都是出于摆脱那些低效和令人困惑virtual的愿望——其他人可能会抓住问题的不同方面并给出完全不同的答案。