4

注意:我正在寻找有关正确搜索词的任何建议,以阅读此类问题。 “对象关系映射”对我来说是一个我可以找到一些好的现有技术的地方......但是我还没有看到任何适合这种情况的东西。)

我有一个非常通用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.

继续这个假设,让我们想象一下,当程序员对他们手中的节点“类型”/标签有更多了解时,我想帮助他们使用不太通用的函数。也许我想帮助他们处理树上的结构习语,比如将字符串项添加到无序列表中,或者将内容提取为字符串。 (这只是一个类比,所以不要太认真地对待例程。)

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 的类而不是继承,并且只是以某种方式与它协作作为“访问器助手”......但我不太了解那会是什么样子。

4

2 回答 2

0

我不确定我是否完全理解您打算做什么,但这里有一些您可能会觉得有用的建议。

肯定在继承的正确轨道上。所有的 UL 节点、LI 节点等都是 Node-s。完美的“is_a”关系,你应该从 Node 类派生这些类。

假设我想帮助程序员在他们了解更多关于他们手中的节点“类型”/标签时使用不太通用的功能

...这就是虚函数的用途。

现在来说maybeCastFromNode方法。那是垂头丧气的。为什么要这么做?也许是为了反序列化?如果是,那么我建议使用dynamic_cast<UnorderedListNode *>. 尽管如果继承树和虚拟方法设计良好,您很可能根本不需要 RTTI。

C++ 继承有时看起来不错,但通常感觉不对。

这可能并不总是 C++ 的错 :-)

于 2012-06-23T20:00:08.907 回答
0

“C++ 继承有时看起来不错,但通常感觉不对。”

的确,这种说法令人担忧:

是什么让节点“maybeCast”成为 UnorderedListNode 与 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 的类而不是继承,并且只是以某种方式与它协作作为“访问器助手”......但我不太了解那会是什么样子。

使用设计模式术语,这匹配类似Proxy的东西。您将有一个轻量级对象来存储指针,然后按值传递。在实践中,处理诸如如何处理const被包装的传入指针的问题可能会很棘手!

这是一个示例,说明在这种情况下如何相对简单地完成它。首先,Accessor 基类的定义:

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”的情况的部分模板特化,这是如何表示它将接收 a 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 () { }
};

“可变访问器”案例的包装器继承自它自己的部分模板特化。通过这种方式,继承提供了适当的强制和分配......禁止将 const 分配给非常量,但反过来工作:

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。, @保罗格罗克

于 2012-07-14T02:03:46.450 回答