C++:此模式是否有名称,是否可以改进
C++: Does this pattern have a name, and can it be improved?
动机
假设我正在编写一个Tree
类。我将用一个Tree::Node
类来表示树的节点。类的方法可能会返回Tree::Node
对象并将它们作为参数,例如获取节点父级的方法:Node getParent(Node)
。
我还想要一个SpecialTree
课。SpecialTree
应该扩展Tree
的接口,并且可以在Tree
所在的任何地方使用。
在幕后,Tree
和SpecialTree
可能具有完全不同的实现。例如,我可能会使用库的GraphA
类来实现Tree
,以便Tree::Node
是GraphA::Node
的精简包装器或typedef。另一方面,SpecialTree
可能是根据GraphB
对象实现的,而Tree::Node
包装GraphB::Node
。
稍后我将提供处理树的函数,例如深度优先搜索函数。此函数应交替接受Tree
和SpecialTree
对象。
模式
我将使用模板化接口类来定义树和特殊树的接口。模板参数将是实现类。例如:
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;
};
并定义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)
。这也抛弃了严格定义的接口,该接口准确描述了Tree
或其后代应该具有的方法。由于SpecialTree
应该始终像Tree
一样,但提供了一些额外的方法,我喜欢使用接口。
与其将TreeInterface
模板参数作为实现,我可以让它采用一个"表示"类来定义Node
的外观(它还必须定义Arc
的外观,等等)。但是由于我可能需要每个实现中的一个,我想我想把它与实现类本身放在一起。
使用此模式可以获得什么?大多数情况下,松散的耦合。如果我想更改Tree
背后的实现,SpecialTree
一点也不介意,因为它只继承接口。
问题
那么,这种模式有名字吗?我通过在ContourTree
中存储指向ContourTreeImplementation
的指针来使用句柄主体模式。但是拥有模板化界面的方法呢?这有名字吗?
有没有更好的方法可以做到这一点?看起来我确实在重复自己,写了很多样板代码,但那些嵌套Node
类给我带来了麻烦。如果Tree::Node
和SpecialTree::Node
有相当相似的实现,我可以在TreeInterface
中为Node
定义一个NodeInterface
接口,并在Tree
和SpecialTree
中覆盖节点类的实现。但事实上,我不能保证这是真的。Tree::Node
可以包装一个GraphA::Node
,SpecialTree::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);
我个人会同意他们的看法。
好的,您担心的是,无法通过类型系统确保调用方传递给depthFirstSearch
的T
实际上符合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
。您同样可以让它返回true
或false
,然后在depthFirstSearch()
中击中static_assert
。
无论如何,这就是我解决问题的方式。请注意,我的整篇文章都是出于摆脱那些低效和令人困惑virtual
的愿望——其他人可能会抓住问题的不同方面并给出完全不同的答案。
- 声明是否有可能逃脱其封闭的名称空间
- 是否有一种方法可以避免标头文件中使用的constexpr函数输入全局范围,而无需额外的名称空间
- 是否有获取 *.exe 文件的产品名称的函数?
- 在C/C 中,是否有可能在编译时将函数的名称放入代码中
- clang,linux是否有一个选项可以在链接时更改共享库名称
- 非 OOP 版本的"财产"是否有名称?
- 是否有一种简单的方法可以在运行时在C 中创建/名称对象
- 是否有任何窗口函数可以使用域名获取所有OU(组织单位)名称
- C++:此模式是否有名称,是否可以改进
- 是否有标准名称(模板或宏)来替换ARRAY_SIZE、_countof等
- 是否有删除半个双向链表边缘的名称
- 是否有技术原因不允许using声明以不同的名称访问函数名
- CM_Get_DevNode_Property_Keys是否有获取密钥名称的方法
- 这种类型的优化是否有一个名称?
- 是否有c++ type_info的可移植包装器来标准化类型名称字符串格式?
- 检查是否有多个字符串为空并打印它们的名称
- 此方法是否已经有名称
- 在名称空间中搜索名称时是否有任何顺序
- 像"__LINE__"一样,C/C++ 中是否有任何打印线程名称或 ID 的标准宏?
- 在std::future::then周围是否有这个方便包装的名称?