条款30:确保目标区间足够大

优质
小牛编辑
140浏览
2023-12-01

条款30:确保目标区间足够大

STL容器在被添加时(通过insert、push_front、push_back等)自动扩展它们自己来容纳新对象。这工作的很好,有些程序员因为这个信仰而被麻痹,认为他们不必担心要为容器中的对象腾出空间,因为容器自己可以照顾好这些。如果是那样就好了!当程序员想向容器中插入对象但并没有告诉STL他们所想的时,问题出现了。这是一个常见的可以自我表现的方法:

int transmogrify(int x);					// 这个函数从x
							// 产生一些新值
vector<int> values;
...							// 把数据放入values
vector<int> results;					// 把transmogrify应用于
transform(values.begin(), values.end(),			// values中的每个对象
		results.end(),				// 把这个返回的values
		transmogrify);				// 附加到results
							// 这段代码有bug!

在本例中,transform被告知它的目的区间是从results.end()开始的,所以那就是开始写在values的每个元素上调用transmogrify的结果的地方。就像所有使用目标区间的算法,transform通过对目标区间的元素赋值的方法写入结果,transform会把transmogrify应用于values[0]并把结果赋给*results.end()。然后它会把transmogrify应用于value[1]并把结果赋给*(results.end()+1)。那只能带来灾难,因为在*results.end()没有对象,*(results.end()+1)也没有!调用transform是错误的,因为它会给不存在的对象赋值。(条款50解释了STL的一个调试实现怎么在运行期检测这个问题。)犯了这种错误的程序员几乎总是以为他们调用算法的结果能插入目标容器。如果那是你想要发生的,你就必须说出来。STL是一个库,不是一个精神。在本例中,说“请把transform的结果放入叫做results容器的结尾”的方式是调用back_inserter来产生指定目标区间起点的迭代器:

vector<int> results;					// 把transmogrify应用于
transform(values.begin(), values.end(),			// values中的每个对象,
			back_inserter(results),		// 在results的结尾
			transmogrify);			// 插入返回的values

在内部,back_inserter返回的迭代器会调用push_back,所以你可以在任何提供push_back的容器上使用back_inserter(也就是任何标准序列容器:vector、string、deque和list)。如果你想让一个算法在容器的前端插入东西,你可以使用front_inserter。在内部,front_inserter利用了push_front,所以front_insert只和提供那个成员函数的容器配合(也就是deque和list):

...							// 同上
list<int> results;						// results现在是list
transform(values.begin(), values.end(),			// 在results前端
			front_inserter(results),		// 以反序
			transmogrify);			// 插入transform的结果

因为front_inserter用push_front把每个对象添加到results,results中的对象顺序会和values中对应的对象顺序相反。这也是为什么front_inserter没有back_inserter那么常用的原因之一。另一个原因是vector不提供push_front,所以front_inserter不能用于vector。

如果你要transform把输出结果放在results前端,但你也要输出和values中对应的对象顺序相同,只要以相反的顺序迭代values:

list<int> results;						// 同上
transform(values.rbegin(), values.rend(),			// 在results前端
			front_inserter(results),		// 插入transform的结果
			transmogrify);			// 保持相对的对象顺序

front_inserter让你强制算法在容器前端插入它们的结果,back_inserter让你告诉它们把结果放在容器后端,有点惊人的是inserter允许你强制算法把它们的结果插入容器中的任意位置:

vector<int> values;					// 同上
...
vector<int> results;					// 同上,除了现在
...							// 在调用transform前
							// results已经有一些数据
transform(values.begin(), values.end(),			// 把transmogrify的
	inserter(results, results.begin() + results.size()/2),	// 结果插入
	transmogrify);					// results的中间

不管你是否使用了back_inserter、front_inserter或inserter,每次对目的区间的插入只完成一个对象。条款5解释了对于连续内存容器(vector、string和deque)来说这可能很昂贵,但条款5的建议解决方法(使用区间成员函数)不能应用于使用算法来完成插入的情况。在本例中,transform会对目的区间每次写入一个值,你无法改变。

当你要插入的容器是vector或string时,你可以通过按照条款14的建议最小化这个代价,预先调用reserve。你仍然要承受每次发生插入时移动元素的开销,但至少你避免了重新分配容器的内在内存:

vector<int> values;					// 同上
vector<int> results;
...
results.reserve(results.size() + values.size());		// 确定results至少
							// 还能装得下
							// values.size()个元素
transform(values.begin(), values.end(),			// 同上,
	inserter(results, results.begin() + results.size() / 2),// 但results
	transmogrify);					// 没有任何重新分配操作

当使用reserve来提高一连串插入的效率时,总是应该记住reserve只增加容器的容量:容器的大小仍然没有改变。即使调用完reserve,当你想要让容器把新元素加入到vector或string时,你也必须对算法使用插入迭代器(比如,从back_inserter、front_inserter或inserter返回的迭代器之一)。

要把这些完全弄清楚,这里有一个提高本条款开始时的那个例子的效率的错误方法(就是我们把transmogrify作用于values里的数据的结果附加到results的那个例子):

vector<int> values;				// 同上
vector<int> results;
...
results.reserve(results.size() + values.size());	// 同上
transform(values.begin(), values.end(),		// 写入transmogrify的结果
		results.end(),			// 到未初始化的内存
		transmogrify);			// 行为未定义!

在这段代码中,transform愉快地试图对results尾部的原始的、未初始化的内存赋值。通常,这会造成运行期错误,因为赋值只在两个对象之间操作时有意义,而不是在一个对象和一块原始的比特之间。即使这段代码碰巧作了你想要它做的事情,results也不会知道transform在它的未使用容量上“创造”的新“对象”。直到results知道之前,它的大小在调用transform后仍然和原来一样。同样的,它的end迭代器仍和调用transform前指向同样的位置。结论呢?使用reserve而没有用插入迭代器会在算法内部导致未定义行为,也会弄乱容器。

正确地写这个例子的代码的方法是使用reserve和插入迭代器:

vector<int> values;				// 同上
vector<int> results;
results.reserve(results.size() + values.size());	// 同上
transform(values.begin(), values.end(),		// 把transmogrify的结果
		back_inserter(results),		// 写入results的结尾,
		transmogrify);			// 处理时避免了重新分配

到目前为止,我都在假设你让像transform那样的算法把它们的结果作为新元素插入容器。这是通常的期望,但有时候你要覆盖现有容器的元素而不是插入新的。当这种情况时,你不需要插入迭代器,但你仍然需要按照本条款的建议来确保你的目的区间足够大。

比如,假设你让transform覆盖results的元素。如果results至少有和values一样多的元素,那很简单。如果没有,你也必须使用resize来确保它有。

vector<int> values;
vector<int> results;
...
if (results.size() < values.size()){		// 确保results至少
	results.resize(values.size());		// 和values一样大
}
transform(values.begin(), values.end(),		// 覆盖values.size()个
		results.begin(),			// results的元素
		transmogrify);

或者你可以清空results然后用通常的方式使用插入迭代器:

...
results.clear();					// 销毁results中
						// 的所有元素
results.reserve(values.size());			// 保留足够空间
transform(values.begin(), values.end(),		// 把transform地返回值
		pack_inserter(results),		// 放入results
		transmogrify);

本条款论证了这个主题的很多变化,但我希望你能牢牢记住本质。无论何时你使用一个要求指定目的区间的算法,确保目的区间已经足够大或者在算法执行时可以增加大小。如果你选择增加大小,就使用插入迭代器,比如ostream_iterators或从back_inserter、front_inserter或inserter返回的迭代器。这是所有你需要记住的东西。

(译注:这个问题在《C++标准程序库》322页也有讨论)