动机

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

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

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

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

图案

我将使用模板化的接口(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;
};

并类似于SpecialTreeSpecialTreeImplementation定义TreeTreeImplementation

我的深度优先搜索功能可能如下所示:
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::NodeSpecialTree::Node具有相当类似的实现,则可以为NodeInterface中的Node定义TreeInterface接口(interface),并覆盖TreeSpecialTree中的节点类的实现。但事实上,我不能保证这是真的。 Tree::Node可以包装GraphA::NodeSpecialTree::Node可以包装整数。因此,这种方法不太有效,但似乎仍有改进的空间。有什么想法吗?

最佳答案

看起来像Curiously Recurring Template PatternPimpl 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);

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

好的,您担心的是,那么就无法通过类型系统确保调用者传递给TdepthFirstSearch实际上符合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。同样可以使它返回truefalse,然后在static_assert中命中depthFirstSearch()

无论如何,这就是我要解决的问题。请注意,我的整个帖子都是出于摆脱那些效率低下和令人困惑的virtual的愿望而来的-其他人可能会陷入问题的另一个方面,并给出完全不同的答案。

09-10 08:59
查看更多