STL
之学习笔记
——尽量使用区间成员函数
使用区间成员函数代替它们的单元素兄弟的理由,
- 一般来说使用区间成员函数可以输入更少的代码。
- 区间成员函数会导致代码更清晰更直接了当。
简而言之,区间成员函数所产生的代码更容易写,更容易理解。不是这样吗?
唉,有些人会把这个论点看作编程风格的问题,而开发者喜欢争论风格问题几乎和喜欢争论什么是真正的编辑器一样。(虽然有很多疑问,但它的确是Emacs。)如果有一个确定区间成员函数优先于它们的单元素兄弟的更普遍赞成的标准将会很有好处。对于标准序列容器,我们有一个:效率。当处理标准序列容器时,应用单元素成员函数比完成同样目的的区间成员函数需要更多地内存分配,更频繁地拷贝对象,而且/或者造成多余操作。
比如,假设你要把一个int数组拷贝到vector前端。(数据可能首先存放在一个数组而不是vector中,因为数据来自于遗留的C API。对于混合使用STL容器和C API的问题的讨论,参见条款16。)使用vector区间insert函数,真的微不足道:
int data[numValues]; // 假设numValues在
// 其他地方定义
vector<int> v;
...
v.insert(v.begin(), data, data + numValues); // 把data中的int
// 插入v前部
在一个显式循环中使用迭代调用来插入,它可能看起来多多少少像这样:
vector<int>::iterator insertLoc(v.begin());
for (int i = 0; i < numValues; ++i) {
insertLoc = v.insert(insertLoc, data[i]);
++insertLoc;
}
注意我们必须小心保存insert返回值以用于下次循环迭代。如果我们在每次插入后没有更新insertLoc,我们就会有两个问题。首先,所有第一次以后的循环迭代会导致未定义行为,因为每次调用insert会使insertLoc无效。第二,即使insertLoc保持有效,我们总是在vector的前部插入(也就是,在v.begin()),这样的结果就是整数以反序拷贝到v中。
copy(data, data + numValues, inserter(v, v.begin()));
这次演示了copy模板,这段代码基于copy,这和使用显式循环的代码几乎一样,所以处于效率分析的目的,我们会关注于显示循环,要牢记分析也是一样有效于使用copy的代码。着眼于显式循环可以更容易地了解效率冲击都在哪里。是的,“冲击”是复数,因为使用insert单元素版本的代码对你征收了三种不同的性能税,而如果你用区间版本的insert,则一种都没有。
第一种税在于没有必要的函数调用。把numValues个元素插入v,每次一个,自然会花费你numValues次调用insert。使用insert的区间形式,你只要花费一次调用,节省了numValues-1次调用。当然,可能的内联会使你节省这种税,但再次说明,它也可能不会。只有一件事情是确定的,使用insert的区间形式,你明确地不必为此花费。
内联也节省不了你的第二种税——无效率地把v中的现有元素移动到它们最终插入后的位置的开销。每次调用insert来增加一个新元素到v,插入点以上的每个元素都必须向上移动一次来为新元素腾出空间。所以在位置p的元素必须向上移动到位置p+1等。在我们的例子中,我们在v的前部插入了numValues个元素。那意味着在v中每个插入之前的元素都得向上移动一共numValues个位置。但每次insert调用时每个只能向上移动一个位置,所以每个元素一共会被移动numValues次。如果v在插入前有n个元素,则一共会发生n*numValues次移动。在这个例子里,v容纳int,所以每次移动可能会归结为一次memmove调用,但如果v容纳了用户自定义类型比如Widget,每次移动会导致调用那个类型的赋值操作符或者拷贝构造函数。(大部分是调用赋值操作符,但每次vector的最后一个元素被移动,那个移动会通过调用元素的拷贝构造函数来完成。)于是在一般情况下,把numValues个新对象每次一个地插入容纳了n个元素的vector<Widget>的前部需要花费n*numValues次函数调用:(n-1)*numValues调用Widget赋值操作符和numValues调用Widget拷贝构造函数。即使这些调用内联了,你仍然做了移动numValues次v中的元素的工作。
相反的是,标准要求区间insert函数直接把现有元素移动到它们最后的位置,也就是,开销是每个元素一次移动。总共开销是n次移动,numValues次容器中的对象类型的拷贝构造函数,剩下的是类型的赋值操作符。相比单元素插入策略,区间insert少执行了n*(numValues-1)次移动。花一分钟想想。这意味着如果numValues是100,insert的区间形式会比重复调用insert的单元素形式的代码少花费99%的移动!