动机
假设我正在编写Tree
类。我将用Tree::Node
类表示树的节点。该类的方法可能会返回Tree::Node
对象,并将其作为参数,例如获取节点父级的方法:Node getParent(Node)
。
我还需要一个SpecialTree
类。 SpecialTree
应该扩展Tree
的接口(interface),并且可以在Tree
所在的任何地方使用。
在后台,Tree
和SpecialTree
可能具有完全不同的实现。例如,我可以使用库的GraphA
类来实现Tree
,以便Tree::Node
是GraphA::Node
的精简包装或typedef。另一方面,SpecialTree
可能是根据GraphB
对象实现的,而Tree::Node
包装了GraphB::Node
。
稍后,我将提供处理树的功能,例如深度优先搜索功能。此函数应互换接受Tree
和SpecialTree
对象。
图案
我将使用模板化的接口(interface)类为一棵树和一棵特殊的树定义接口(interface)。模板参数将是实现类。例如:
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(); }
};
然后,我可以从
SpecialTreeInterface
导出TreeInterface
:template <typename Implementation>
class SpecialTreeInterface : public TreeInterface<Implementation>
{
virtual void specialTreeFunction() = 0;
};
并类似于
SpecialTree
和SpecialTreeImplementation
定义Tree
和TreeImplementation
。我的深度优先搜索功能可能如下所示:
template <typename T>
void depthFirstSearch(TreeInterface<T>& tree);
并且因为
SpecialTree
源自TreeInterface
,所以这将适用于Tree
对象和SpecialTree
对象。备择方案
一种替代方法是更加依赖模板,以使
SpecialTree
在类型层次结构中根本不是TreeInterface
的后代。在这种情况下,我的DFS函数将类似于template <typename T> depthFirstSearch(T& tree)
。这也抛出了严格定义的接口(interface),该接口(interface)确切描述了Tree
或其后代应采用的方法。由于SpecialTree
应该始终像Tree
一样工作,但是提供了一些其他方法,因此我喜欢使用接口(interface)。代替
TreeInterface
模板参数作为实现,我可以让它采用一个“表示”类,该类定义Node
的外观(还必须定义Arc
的外观,依此类推)。但是,由于每个实现可能都需要其中之一,因此,我想将其与实现类本身保持在一起。通过使用这种模式,我可以获得什么?通常,是较松散的联轴器。如果我想更改
Tree
后面的实现,SpecialTree
根本不在乎,因为它仅继承了接口(interface)。问题
那么,该模式有名称吗?我通过在
ContourTreeImplementation
中存储指向ContourTree
的指针来使用句柄主体模式。但是拥有模板化接口(interface)的方法又如何呢?这有名字吗?有一个更好的方法吗?看来我经常重复自己,并编写了许多样板代码,但是这些嵌套的
Node
类给我带来了麻烦。如果Tree::Node
和SpecialTree::Node
具有相当类似的实现,则可以为NodeInterface
中的Node
定义TreeInterface
接口(interface),并覆盖Tree
和SpecialTree
中的节点类的实现。但事实上,我不能保证这是真的。 Tree::Node
可以包装GraphA::Node
,SpecialTree::Node
可以包装整数。因此,这种方法不太有效,但似乎仍有改进的空间。有什么想法吗? 最佳答案
看起来像Curiously Recurring Template Pattern和Pimpl idiom的混合物。
在CRTP中,我们从Tree
导出TreeInterface<Tree>
;在您的代码中,您是从Tree
派生TreeInterface<TreeImplementation>
的。因此,就像@ElliottFrisch所说的:这是strategy pattern的应用程序。代码的某些部分关心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);
我个人会同意他们的看法。
好的,您担心的是,那么就无法通过类型系统确保调用者传递给
T
的depthFirstSearch
实际上符合TreeInterface
。好吧,我认为执行该限制的最C++-ish方式将是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");
...
}
请注意,如果
conforms_to_TreeInterface<T>()
不符合要求,我的T
实际上将静态声明为失败;它永远不会真正返回false
。同样可以使它返回true
或false
,然后在static_assert
中命中depthFirstSearch()
。无论如何,这就是我要解决的问题。请注意,我的整个帖子都是出于摆脱那些效率低下和令人困惑的
virtual
的愿望而来的-其他人可能会陷入问题的另一个方面,并给出完全不同的答案。