(注意:我正在寻找有关正确搜索词的任何建议,以阅读此类问题。 "对象关系映射"对我来说是一个可以找到一些好的现有技术的地方......但我还没有看到任何适合这种情况的东西。
我有一个非常通用的class Node
,目前你可以认为它有点像DOM树中的一个元素。 这并不完全是正在发生的事情 - 它们是内存映射文件中的图形数据库对象。 但是对于所有实际目的来说,这个类比都相当接近,所以为了简单起见,我将坚持使用 DOM 术语。
嵌入在节点中的"标签"意味着您应该(理想情况下)能够使用它执行的一组特定操作。 现在我正在使用派生类来执行此操作。 因此,例如,如果您尝试表示类似HTML列表的内容:
<ul>
<li>Coffee</li>
<li>Tea</li>
<li>Milk</li>
</ul>
基础树将是七个节点:
+--UL // Node #1
+--LI // Node #2
+--String(Coffee) // Node #3 (literal text)
+--LI // Node #4
+--String(Tea) // Node #5 (literal text)
+--LI // Node #6
+--String(Milk) // Node #7 (literal text)
由于 getString()
已经是节点本身的原始方法,因此我可能只会制作 class UnorderedListNode : public Node
、class ListItemNode : public Node
。
继续这个假设,让我们想象一下,当程序员更多地了解他们手中的 Node "类型"/标签时,我想帮助程序员使用不太通用的函数。 也许我想帮助他们在树上使用结构习语,例如将字符串项添加到无序列表中,或将内容提取为字符串。 (这只是一个类比,所以不要太认真地对待例程。
class UnorderedListNode : public Node {
private:
// Any data members someone put here would be a mistake!
public:
static boost::optional<UnorderedListNode&> maybeCastFromNode(Node& node) {
if (node.tagString() == "ul") {
return reinterpret_cast<UnorderedListNode&>(node);
}
return boost::none;
}
// a const helper method
vector<string> getListAsStrings() const {
vector<string> result;
for (Node const* childNode : children()) {
result.push_back(childNode->children()[0]->getText());
}
return result;
}
// helper method requiring mutable object
void addStringToList(std::string listItemString) {
unique_ptr<Node> liNode (new Node (Tag ("LI"));
unique_ptr<Node> textNode (new Node (listItemString));
liNode->addChild(std::move(textNode));
addChild(std::move(liNode));
}
};
将数据成员添加到这些新的派生类是一个坏主意。 真正持久化任何信息的唯一方法是使用 Node 的基础例程(例如,上面的 addChild
调用或getText
)与树进行交互。 因此,真正的继承模型 - 在某种程度上存在 - 在C++类型系统之外。 是什么让<UL>
节点"maybeCast"成一个UnorderedListNode
与vtables/etc无关。
C++继承有时看起来是对的,但通常感觉不对。 我觉得我应该拥有独立于 Node 存在的类,而不是继承,并且只是以某种方式作为"访问器助手"与它协作......但我不太了解那会是什么样子。
我不确定我是否完全理解了您的意图,但这里有一些您可能会发现有用的建议。
您绝对走在继承的正确轨道上。所有UL节点,LI节点,...等是节点。完美的"is_a"关系,你应该从 Node 类派生这些类。
假设我想帮助程序员使用不太通用的功能,当他们更多地了解他们手中的节点"类型"/标签时。
。这就是虚拟功能的用途。
现在是maybeCastFromNode
方法。这太令人沮丧了。你为什么要这样做?也许是为了反序列化?如果是,那么我建议使用 dynamic_cast<UnorderedListNode *>
.尽管如果继承树和虚拟方法设计良好,则很可能根本不需要 RTTI。
C++继承有时看起来是对的,但通常感觉不对。
这可能并不总是C++的错:-)
"C++遗产有时看起来是对的,但通常感觉不对劲。
事实上,这种说法令人担忧:
是什么让一个节点"maybeCast"成一个无序列表节点与vtables/etc无关。
如下代码:
static boost::optional<UnorderedListNode&> maybeCastFromNode(Node& node) {
if (tagString() == "ul") {
return reinterpret_cast<UnorderedListNode&>(node);
}
return boost::none;
}
(1)类型双关语
如果传入的Node&
是通过未在继承路径上合法且正确地构造UnorderedListNode
的机制分配的,这就是所谓的类型双关语。 这几乎总是一个坏主意。 即使大多数编译器上的内存布局在没有虚函数且派生类不添加数据成员时似乎有效,它们在大多数情况下都可以自由破坏它。
(2) 严格别名
接下来是编译器的假设,即指向根本不同类型的对象的指针不会相互"别名"。 这是严格的别名要求。 虽然它可以通过非标准扩展禁用,但这应该只应用于遗留情况......它阻碍了优化。
从学术角度来看,目前还不完全清楚这两个障碍在特殊情况下是否有规范允许的解决方法。 这里有一个调查这个问题的问题,在撰写本文时仍然是一个公开的讨论:
仅通过指针转换使类类型可互换,而无需分配任何新对象?
但引用@MatthieuM的话:"你越接近规范的边缘,你就越有可能遇到编译器错误。因此,作为工程师,我建议务实,避免与编译器玩心理游戏;你是对还是错无关紧要:当你在生产代码中崩溃时,你输了,而不是编译器编写者。
这可能更正确:
我觉得我应该拥有独立于 Node 存在的类,而不是继承,并且只是以某种方式作为"访问器助手"与它协作......但我不太了解那会是什么样子。
使用设计模式术语,这与代理之类的东西相匹配。 您将拥有一个轻量级对象,用于存储指针,然后按值传递。 在实践中,处理诸如如何处理被包装的传入指针的const
等问题可能会很棘手!
下面是如何相对简单地处理这种情况的示例。 首先,访问器基类的定义:
template<class AccessorType> class Wrapper;
class Accessor {
private:
mutable Node * nodePtrDoNotUseDirectly;
template<class AccessorType> friend class Wrapper;
void setNodePtr(Node * newNodePtr) {
nodePtrDoNotUseDirectly = newNodePtr;
}
void setNodePtr(Node const * newNodePtr) const {
nodePtrDoNotUseDirectly = const_cast<Node *>(newNodePtr);
}
Node & getNode() { return *nodePtrDoNotUseDirectly; }
Node const & getNode() const { return *nodePtrDoNotUseDirectly; }
protected:
Accessor() {}
public:
// These functions should match Node's public interface
// Library maintainer must maintain these, but oh well
inline void addChild(unique_ptr<Node>&& child)) {
getNode().addChild(std::move(child));
}
inline string getText() const { return getNode().getText(); }
// ...
};
然后,用于处理包装"const Accessor"情况的部分模板专用化,即如何发出它将接收const Node &
的信号:
template<class AccessorType>
class Wrapper<AccessorType const> {
protected:
AccessorType accessorDoNotUseDirectly;
private:
inline AccessorType const & getAccessor() const {
return accessorDoNotUseDirectly;
}
public:
Wrapper () = delete;
Wrapper (Node const & node) { getAccessor().setNodePtr(&node); }
AccessorType const * operator-> const () { return &getAccessor(); }
virtual ~Wrapper () { }
};
"可变访问器"案例的包装器继承自其自己的部分模板专用化。 通过这种方式,继承提供了适当的胁迫和转让......禁止将常量分配给非常量,但反过来工作:
template<class AccessorType>
class Wrapper : public Wrapper<AccessorType const> {
private:
inline AccessorType & getAccessor() {
return Wrapper<AccessorType const>::accessorDoNotUseDirectly;
}
public:
Wrapper () = delete;
Wrapper (Node & node) : Wrapper<AccessorType const> (node) { }
AccessorType * operator-> () { return &Wrapper::getAccessor(); }
virtual ~Wrapper() { }
};
使用测试代码和注释记录奇怪部分的编译实现位于此处的 Gist 中。
资料来源:@MatthieuM、@PaulGroke