65.9K
CodeProject 正在变化。 阅读更多。
Home

auto_ptr 的容器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.17/5 (14投票s)

2003年10月15日

CPOL

5分钟阅读

viewsIcon

85109

本文描述了如何使用 auto_ptr 来解决一些问题。

引言

如何创建一个包含不同数据类型,或者更准确地说,是包含不同类对象的数组,这是一个能让一些人感到困惑的有趣问题。一些聪明的程序员的答案是创建一个包含所有这些不同类的基类,然后创建一个该基类指针的数组,接着创建相应的对象并将其地址存储在数组的基类指针中。也许一个更聪明的程序员会建议使用基类指针的 vector 而不是数组。在创建这个之前,请看一下 Scott Meyers 在“Effective STL”第 7 条中的建议,即“在使用 newed 指针的容器时,请记住在容器销毁之前删除这些指针”[MEY01]。

好的,你同意使用简单的循环或者创建一个谓词并使用 for_each 来删除存储在容器中的所有指针所指向的元素。但是你仍然需要牢记一点,那就是你的基类的析构函数必须是“虚”的,否则派生类的析构函数将不会被调用。好吧,如果你已经决定在基类中创建虚析构函数,那么你的代码将是这样的:

    std::vector<Base*> vecBase;
    // initialize it with appropriate drive class object
    // now delete all new object
    for (std::vector<Base*>::iterator iter_ = 
        vecBase.begin(); 
        iter_ != vecBase.end();
        ++iter_
    {
        delete *iter_;
    }

即使你决定为你的基类创建虚析构函数,仍然有一种情况使得这个解决方案不好,那就是指向 STL 容器类,如 string。因为没有 STL 类拥有虚析构函数,所以你无法将其应用于 STL 容器。这段代码即使删除了所有元素,如果从 std::string 继承类并存储其地址,会明显地造成资源泄漏。

    std::vector<std::string*> vecString;

好的,我们同意创建虚析构函数,并且不使用 STL 容器类指针。但是这个解决方案仍然有一个问题,那就是异常安全。假设由于任何原因,在删除循环期间或删除循环之前抛出了异常,那么所有元素都不会被删除。在动态创建的对象和异常安全的情况下,最聪明的解决方案是智能指针,即 auto_ptr。所以解决方案是 auto_ptr 容器(COAP),但是等等,COAP 不是一个可移植的解决方案,并且这段代码不应该被编译。在详细讨论它之前,让我们先快速回顾一下 auto_ptr

什么是 auto_ptr?在讨论 auto_ptr 是什么之前,先看一下 auto_ptr 解决的问题?看一下这段简单的代码,我们在其中分配一个对象,在删除它之前对其进行一些处理。

    int* pInt = new int;
    // do some work on pInt
    delete pInt;

好吧,这段代码非常直接,在正常流程中没有任何问题。但是如果对象被删除之前抛出了异常,会发生什么?这显然是资源泄漏。这时 auto_ptr 就派上用场了。它只是一个模板类,它分配一个对象,提供类似指针的接口,并在其析构函数中清除分配的对象。现在我们的代码变成这样:

    std::auto_ptr<int> ptInt(new int);
    // do work on ptInt and no need to call delete;

现在不再需要调用 delete,因为智能指针负责在它离开作用域时执行此操作,无论代码流程是正常的还是抛出了异常。C++ Gotchas 中的第 68 条“Improper Use of auto_ptr”[DEW02] 也将这个问题作为一个常见的 C++ Gotcha 来讨论。auto_ptr 最常见的误用是分配对象数组而不是单个对象。

    std::auto_ptr<int> ptIntArr (new int[iLen]);

最好的替代解决方案是使用 vector 而不是动态分配的数组本身。但是如果你仍然有兴趣使用 auto_ptr,那么可以看一下 More Exceptional C++ [SUT01] 中的第 29 条“Using auto_ptr”讨论的适配器模式技术。

那么 auto_ptr 有什么问题呢?auto_ptr 持有它所指向对象的拥有权。但是当复制 auto_ptr 时,它具有所有权转移语义。换句话说,当你复制 auto_ptr 时,你将自动将对象的所有权从源转移到目标,并且源 auto_ptr 会变成 NULL。这就是为什么 auto_ptr 的复制构造函数和赋值运算符不使用同对象的 const 引用,就像其他复制构造函数和赋值运算符一样。

    template<typename X>
    class auto_ptr
    {
        explicit auto_ptr(X* p = 0) throw();

        // don't not have const reference as parameter
        auto_ptr (auto_ptr&) throw();
        auto_ptr& operator = (auto_ptr&) throw();

        // template version of copy constructor 
        // and assignment operator

        // other functions
    }

但是这种语义的必要性是什么呢?请设想一下,如果 auto_ptr 没有这种语义,并且复制后源和目标都持有同一个对象,那么看看下面的代码

    void f()
    {
        auto_ptr<int> ptInt1(new Int);
        // copy constructor
        auto_ptr<int> ptInt2 (pInt1);
        auto_ptr<int> ptInt3;
        // assignment operator
        ptInt3 = pInt2;
    }

现在,如果源和目标都持有同一个对象,那么当函数结束并且 pInt1pInt2pInt3 离开作用域时,所有对象的析构函数都会被调用。除了第一个之外,每个析构函数都试图删除同一个对象,即一个已经被删除的对象。但幸运的是,auto_ptr 具有转移所有权语义,因此在这个例子中 ptInt1ptInt2NULL,删除 NULL 是安全的。换句话说,在 auto_ptr 的情况下,它不是真正的复制,即复制操作后源和对象不再相同。如果你不想改变 auto_ptr 的所有权,就让它成为 const,当然 const 也有其他限制,更多信息请参见 Exceptional C++ [SUT00] 中的第 37 条“auto_ptr”。

现在回到 STL 容器。所有 STL 容器都具有复制语义而非引用语义,这意味着当你将任何东西插入任何 STL 容器时,都会创建一个它的副本并放入容器中,当你从容器中获取任何东西时,情况也一样。有关复制语义的更多信息,请参见 Effective STL [MEY01] 中的第 3 条。但不幸的是,auto_ptr 的语义不满足 STL 容器的复制要求,因此不建议在容器中使用它。如果你尝试这样做,可能会得到不可预测的结果,例如容器中的一些或全部值会因为所有权转移语义而变成 NULL,因为我们无法提前知道特定算法的内部工作原理。

参考文献

  1. [DEW02] C++ Gotchas, Avoiding Common Problems in Coding and Design Stephen C. Dewhurst, 2002
  2. [MEY01] Effective STL Scott Meyers, 2001
  3. [SUT00] Exceptional C++ 47 Engineering puzzles, programming problems and solutions Herb Sutter, 2000
  4. [SUT01] More Exceptional C++ 40 New Engineering puzzles, programming problems and solutions Herb Sutter, 2001
© . All rights reserved.