Effective STL [8] | 永不建立auto_ptr的容器

警告
本文最后更新于 2023-07-29,文中内容可能已过时。

拷贝一个auto_ptr将改变它的值

当你拷贝一个auto_ptr时,auto_ptr所指向对象的所有权被转移到拷贝的auto_ptr,而被拷贝的auto_ptr被设为NULL

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Widget {
 public:
  explicit Widget(int in) : randy(in) {}
  inline bool operator<(Widget& in) { return randy < in.randy; }

 public:
  int randy;
};

auto_ptr<Widget> pw1(new Widget); // pw1指向一个Widget
auto_ptr<Widget> pw2(pw1); // pw2指向pw1的Widget; pw1被设为NULL。(Widget的所有权从pw1转移到pw2。)
pw1 = pw2; // pw1现在再次指向Widget; pw2被设为NULL

有意思的是,如果你建立一个auto_ptr<Widget>vector,然后使用一个指向的Widget的值的函数对它进行排序:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
bool widgetAPCompare(const auto_ptr<Widget>& lhs, const auto_ptr<Widget>& rhs) {
 return *lhs < *rhs; // 假设Widget 存在operator<
}
auto_ptr<Widget> w1(new Widget(3));
auto_ptr<Widget> w2(new Widget(2));
widgets.push_back(w1);
widgets.push_back(w2);
vector<auto_ptr<Widget> > widgets; // 建立一个vector,然后用Widget的auto_ptr填充它;
// 记住这将不能编译!
sort(widgets.begin(), widgets.end(), widgetAPCompare);// 排序这个vector

这段代码将不能编译

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
warning: ‘template<class> class std::auto_ptr’ is deprecated [-Wdeprecated-declarations]
   30 |   std::vector<auto_ptr<Widget> >
      |               ^~~~~~~~
In file included from /usr/include/c++/9/memory:80,
                 from temp.cpp:10:
/usr/include/c++/9/bits/unique_ptr.h:53:28: note: declared here
   53 |   template<typename> class auto_ptr;
      |                            ^~~~~~~~
temp.cpp:33:3: warning: ‘template<class> class std::auto_ptr’ is deprecated [-Wdeprecated-declarations]
   33 |   auto_ptr<Widget> w1(new Widget(3));
      |   ^~~~~~~~

从概念上看所有东西也都很合理,但结果却完全不合理。例如,在排序过程中widgets中的一个或多个auto_ptr可能已经被设为NULL。

排序这个vector的行为可能已经改变了它的内容!

剖析

实现sort的方法是使用了快速排序算法的某种变体。

排序一个容器的基本思想是,选择容器的某个元素作为“主元”,然后对大于和小于或等于主元的值进行递归排序。

在sort内部,这样的方法看起来像这样:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

template<class RandomAccessIterator, class Compare>// 这个sort的声明直接来自于标准
void sort(RandomAccessIterator first, RandomAccessIterator last, Compare comp)
{
    // 这个typedef在下面解释
    typedef typename iterator_traits<RandomAccessIterator>::value_type ElementType;
    RandomAccessIterator i;
    ... // 让i指向主元
    ElementType pivotValue(*); // 把主元拷贝到一个局部临时变量中;
    ... // wor
}

源码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
template<typename _RandomAccessIterator, typename _Compare>
inline void
sort(_RandomAccessIterator __first, _RandomAccessIterator __last,
 _Compare __comp)
{
  // concept requirements
  __glibcxx_function_requires(_Mutable_RandomAccessIteratorConcept<
    _RandomAccessIterator>)
  __glibcxx_function_requires(_BinaryPredicateConcept<_Compare,
    typename iterator_traits<_RandomAccessIterator>::value_type,
    typename iterator_traits<_RandomAccessIterator>::value_type>)
  __glibcxx_requires_valid_range(__first, __last);
  __glibcxx_requires_irreflexive_pred(__first, __last, __comp);

  std::__sort(__first, __last, __gnu_cxx::__ops::__iter_comp_iter(__comp));
}

// 上面 __gnu_cxx::__ops::__iter_comp_iter(__comp) 的实现如下
  template<typename _Compare, typename _Iterator>
    inline _Iter_comp_to_iter<_Compare, _Iterator>
    __iter_comp_iter(_Iter_comp_iter<_Compare> __comp, _Iterator __it)
    {
      return _Iter_comp_to_iter<_Compare, _Iterator>(
   _GLIBCXX_MOVE(__comp._M_comp), __it); // 这里有move
    }

当涉及iterator_traits<RandomAccessIterator>::value_type时,必须在它前面写上typename,因为它是一个依赖于模板参数类型的名字,在这里是RandomAccessIterator

上面代码中棘手的是这一行:

1
ElementType pivotValue(*i);

因为它把一个元素从保存的区间拷贝到局部临时对象中。

在例子里,这个元素是一个auto_ptr<Widget>,所以这个拷贝操作默默地把被拷贝的auto_ptr——vector中的那个——设为NULL

另外,当pivotValue出了生存期,它会自动删除指向的Widget。这时sort调用返回了,vector的内容已经改变了,而且至少一个Widget已经被删除了。

也可能有几个vector元素已经被设为NULL,而且几个widget已经被删除,因为快速排序是一种递归算法,递归的每一层都会拷贝一个主元。

结论

智能指针的容器是很好的, 但是auto_ptr完全不是那样的智能指针

Buy me a coffee~
支付宝
微信
0%