【问题标题】:Adapter design pattern and memory management in C++C++ 中的适配器设计模式和内存管理
【发布时间】:2011-05-24 19:32:01
【问题描述】:

考虑以下情况:我有一个 C++ 模块,它以 XML 节点作为输入,将其转换为其他数据结构,然后返回结果。

现在这个模块是使用TinyXML 实现的,所以它把TinyXML 类作为输入(特别是一个TiXmlNode)。这是一个问题,因为它迫使任何想要使用我的模块的人使用 TinyXML 来表示整个文档树。例如,如果RapidXML 用户想要使用我的模块,他将无法使用,因为该模块需要一个 TinyXML 节点(以及后续的子节点),而不是 RapidXML 节点。显然,这是一个糟糕的设计,因为可重用性差。

为了解决这个问题,我愿意申请Dependency inversion principle。所以我设计了这个极其简化的、类似 DOM 的界面:

class Node
{
    public:
        enum Type { TYPE_ELEMENT, TYPE_TEXT };

        virtual ~Node() { }
        virtual Type getType() = 0;
        virtual Node* getParentNode() = 0;
        virtual Node* getPreviousSibling() = 0;
        virtual Node* getNextSibling() = 0;
};

class Element : public Node
{
    public:
        virtual ~Element() { }
        virtual const char* getName() = 0;
        virtual Node* getFirstChild() = 0;
        virtual Node* getLastChild() = 0;
        virtual const char* getAttribute(const char* name) = 0;
};

class Text : public Node
{
    public:
        virtual ~Text() { }
        virtual const char* getText() = 0;
};

然后我使用Adapter 设计模式实现这些接口,包装等效的TinyXML 类。或者至少我尝试:

class AdapterNode : public Node
{
    private:
        const TiXmlNode& node;
    public:
        AdapterNode(const TiXmlNode& node) : node(node) { }
        virtual Node* getFirstChild() { Uh... oh, wait.

你可以看到我正在进入一个内存管理的噩梦:我应该在这里返回一个预先存在的、已经分配的节点。事实上,我有一个预先存在的、已分配的 TiXmlNode(可通过node->FirstChild() 访问),但这是一个 TiXmlNode,而不是一个节点!写入return new AdapterNode(node->FirstChild()) 会导致明显的内存泄漏,因为新分配的AdapterNode 永远不会被任何人获得delete

我想到了一些解决这个内存管理问题的解决方案,但大多数都有些难看。我在这里寻求建议:对于这种情况,您首选的解决方案是什么?

【问题讨论】:

    标签: c++ oop memory-management inversion-of-control adapter


    【解决方案1】:

    简单的解决方案:使用智能指针。(智能指针是确定对象生命周期的“简单”方法)

    例如,这里最简单的方法是使用 new 创建对象,然后在 std::shared_ptr(或 boost::shared_ptr)中返回它。

    优化解决方案:使用对象池并提供指针或引用类型,确保您无法删除提供的对象。

    取决于您的数据:通过副本返回。

    【讨论】:

    • 第三个选项是不可能的,因为 Node 是抽象的。
    • 就界面而言,您的第一个选项类似于让 Node 的用户在使用完节点后删除它们。这是完全可行的,但这意味着每次调用 get*() 时,都会分配一个新节点,从而导致巨大的堆活动(数千个微小的 8 字节左右的分配)。当然它会起作用,但这非常低效。这就是为什么我一直在寻找更优雅的东西。
    • 您的第二个选项听起来相当复杂,因为它应该是 TinyXML 的薄包装。
    • 第一个解决方案并不总是意味着创建一个新节点,如果您将它们保存在您的类中带有 shared_ptr 的某个地方,并且如果它们已经构建,则提供它们。通过保留一个 shared_ptr 并将 shared_ptr 提供给外部代码,您可以确保每个人都有一个有效的实例,直到没有人使用它。第二种解决方案是,即使很薄,您也想要性能(速度和内存)优化。
    【解决方案2】:

    选择方法最显着的缺点之一是抽象层的开发人员承担了重大负担。我会说这就像把 RapidXml 类变成 TinyXml 一样难,所以你不会用这个新的抽象层得到任何东西。顺便说一句,您在尝试从 Node::getFirstChild 方法返回 'new AdapterNode(node->FirstChild())' 时已经犯了一个错误:如果第一个孩子是一个元素,您将无法将 AdapterNode 转换为 AdapterElement。还有性能和内存开销,可能会限制可重用性以及绑定到特定的 XML 库。

    我想到了一些东西,比如让 Node(以及 Element、Text 等)本身成为底层实现的智能指针。例如:

    class XmlApi; class Node { public: void * getLowLevelInterface() const; XmlApi * getApi() const; ~Node(); Node(Node const &); Node const & operator =(Node const &); private: void * lowLevelInterface; XmlApi * api; }; class Element : Node { }; class Text : Node { }; class XmlApi { public: // Node -> more specific interface conversions. virtual bool ToElement(Node const &, Element &) const = 0; virtual bool ToText(Node const &, Text &) const = 0; // Node memory management virtual void acquireNode(Node const &) const = 0; virtual void releaseNode(Node const &) const = 0; // Node API enum Type { TYPE_ELEMENT, TYPE_TEXT }; virtual Type getType(Node const &) const = 0; virtual Node getParentNode(Node const &) const = 0; virtual Node getPreviousSibling(Node const &) const = 0; virtual Node getNextSibling(Node const &) const = 0; // Element API virtual char const * getName(Element const &) const = 0; virtual Node getFirstChild(Element const &) const = 0; virtual Node getLastChild(Element const &) const = 0; virtual char const * getAttribute(Element const &, char const *) const = 0; // etc. };

    当然,为从 XmlApi 到 Node、Element 和 Text 的适当方法添加内联包装器是有意义的,但这只是一个次要的实现细节。

    这里的主要优点是开销要少得多(它仍然存在,例如将字符串转换为 char const *)并且为特定的 Xml 库实现实现要简单得多。您只需解压缩“低级”节点指针,将其传递给“低级”API 并打包结果。没有(几乎没有,参见字符串转换)内存管理或复杂的继承层次结构。 Node、Element等类的实现是固定的,只需要编写一次,唯一变化的部分就是XmlApi。

    UPD:如果您可以使用模板使您的库成为仅标头,您甚至可以通过将 XmlApi 转换为模板参数来消除所有虚拟方法调用开销。

    【讨论】:

    • "顺便说一句,你已经犯了一个错误,试图从 Node::getFirstChild 方法返回 'new AdapterNode(node->FirstChild())':如果第一个孩子是你的元素'将无法将 AdapterNode 转换为 AdapterElement。”哦,确实,谢谢。好吧,当我看到它时,我可能会投入一个开关(尽管我不确定它是否能很好地概括)。
    • "如果您可以使用模板使您的库成为仅标题" 好吧,如果我能做到,我什至不会问这个问题...
    • "converting strings to char const *" 您假设实现在内部处理 STL 字符串。情况不一定如此;事实上,TinyXML 在其默认配置中并不使用 STL 字符串。
    • @e-t172:我认为模板本身并不能解决这个问题。您仍然需要某种抽象层,不是吗?
    • @e-t172:我假设您希望为尽可能多的 Xml 库提供抽象层。然后你必须处理来自 MSXML 的 BSTR、来自 xerces-c 的 XMLCh const */XMLString 等等。
    【解决方案3】:

    你应该做的是一次创建整个树,从某个主对象中拥有它,然后从中返回预分配的节点。

    XMLTree xml("xmlfilename.xml");
    Node* ptr = xml.GetNode("beginning");
    

    另外,比起某种getType() 函数,更喜欢使用强类型和dynamic_cast

    【讨论】:

    • dynamic_cast 可能无法跨 dll/so 边界工作。此外,getType 是标准化 DOM API 的一部分,在这里一致性可能比理论上的纯度/优雅/美丽更好。
    • 康斯坦丁·奥兹诺比欣是对的。关于您的解决方案:TinyXML 已经这样做了。我不确定我是否理解你的提议。当然,我可以在 Adapter 类中复制由 TinyXML 构建的整个树,但是对于应该是一个薄包装器的东西来说,这听起来有点矫枉过正。而且内存效率低。
    猜你喜欢
    • 2013-01-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-25
    相关资源
    最近更新 更多