条款8:永不建立auto_ptr的容器 - Effective STL 中文版

返回介绍

条款8:永不建立auto_ptr的容器

发布于 2019-08-07 字数 4551 浏览 1015 评论 0

条款8:永不建立auto_ptr的容器

坦白地说,本条款不需要出现在《Effective STL》里。auto_ptr的容器(COAPs)是禁止的。试图使用它们的代码都不能编译。C++ 标准委员会花费了无数努力来安排这种情况[1]。我本来不需要说有关COAPs的任何东西,因为你的编译器对这样的容器应该有很多抱怨,而且所有那些都是不能编译的。

唉,很多程序员使用STL平台不会拒绝COAPs。更糟的是,很多程序员妄想地把COAPs看作简单、直接、高效地解决经常伴随指针容器(参见条款7和33)资源泄漏的方案。结果,很多程序员被引诱去使用COAPs,即使建立它们不应该成功。

我会马上解释COAPs的幽灵有多令人担心,以至于标准化委员会采取特殊措施来保证它们不合法。现在,我要专注于一个不需要auto_ptr甚至容器知识的缺点:COAPs不可移植。它们能是怎么样的?C++标准禁止他们,比较好的STL平台已经实现了。可以有足够理由推断随着时间的推移,目前不能实现标准的这个方面的STL平台将变得更适应,并且当那发生时,使用COAPs的代码将更比现在更不可移植。如果你重视移植性(并且你应该是),你将仅仅因为它们的移植测试失败而拒绝COAPs。

但可能你没有移植性思想。如果是这样,请允许我提醒你拷贝auto_ptr的独特——有的人说是奇异——的定义。

当你拷贝一个auto_ptr时,auto_ptr所指向对象的所有权被转移到拷贝的auto_ptr,而被拷贝的auto_ptr被设为NULL。你正确地说一遍:拷贝一个auto_ptr将改变它的值:

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

这非常不寻常,也许它很有趣,但你(作为STL的用户)关心的原因是它导致一些非常令人惊讶的行为。例如,考虑这段看起来很正确的代码,它建立一个auto_ptr<Widget>的vector,然后使用一个比较指向的Widget的值的函数对它进行排序。

bool widgetAPCompare(const auto_ptr<Widget>& lhs, 
			const auto_ptr<Widget>& rhs) {
	return *lhs < *rhs;		// 对于这个例子,假设Widget
}					// 存在operator<

vector<auto_ptr<Widget> > widgets;		// 建立一个vector,然后
					// 用Widget的auto_ptr填充它;
					// 记住这将不能编译!
sort(widgets.begin(), widgets.end(),		// 排序这个vector 
			widgetAPCompare);

这里的所有东西看起来都很合理,而且从概念上看所有东西也都很合理,但结果却完全不合理。例如,在排序过程中widgets中的一个或多个auto_ptr可能已经被设为NULL。排序这个vector的行为可能已经改变了它的内容!值得去了解这是怎么发生的。

它会这样是因为实现sort的方法——一个常见的方法,正如它呈现的——是使用了快速排序算法的某种变体。我们不关心快速排序的妙处,但排序一个容器的基本思想是,选择容器的某个元素作为“主元”,然后对大于和小于或等于主元的值进行递归排序。在sort内部,这样的方法多多少少看起来像这样:

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

除非你是在阅读STL源代码方面很有经验,否则这看起来可能有些麻烦,但其实并不坏。唯一的难点是引用了iterator_traits<RandomAccessIterator>::value_type,而只不过是传给sort的迭代器所指向的对象类型的怪异的STL方式。(当我们涉及iterator_traits<RandomAccessIterator>::value_type时,我们必须在它前面写上typename,因为它是一个依赖于模板参数类型的名字,在这里是RandomAccessIterator。更多关于typename用法的信息,翻到第7页。)

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

ElementType pivotValue(*i);

因为它把一个元素从保存的区间拷贝到局部临时对象中。在我们的例子里,这个元素是一个auto_ptr<Widget>,所以这个拷贝操作默默地把被拷贝的auto_ptr——vector中的那个——设为NULL。另外,当pivotValue出了生存期,它会自动删除指向的Widget。这时sort调用返回了,vector的内容已经改变了,而且至少一个Widget已经被删除了。也可能有几个vector元素已经被设为NULL,而且几个widget已经被删除,因为快速排序是一种递归算法,递归的每一层都会拷贝一个主元。

这落入了一个很讨厌的陷阱,这也是为什么标准委员会那么努力地确保你不会掉进去。通过永不建立auto_ptr的容器来尊重它对你的利益的工作,即使你的STL平台允许那么做。

如果你的目标是智能指针的容器,这不意味着你不走运了。智能指针的容器是很好的,条款50描述了你在哪里可以找到和STL容器咬合良好的智能指针。只不过auto_ptr不是那样的智能指针。完全不是。


[1] 如果你对auto_ptr标准化的痛苦历程感兴趣,把你的web浏览器指向More Effective C++网站的auto_ptr更新页[29]。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

支持 Markdown 语法,需要帮助?

目前还没有任何评论,快来抢沙发吧!