STL三十条
+ -
管理员 VIP用户 编辑
2023-10-16 2 0

条款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返回的迭代器。这是所有你需要记住的东西。

插入辅文

条款1:仔细选择你的STL容器
你知道C++中有很多你可以支配的容器,但是你意识到有多少吗?要确定你没有忽略你的选项,这里有一个快速回顾。标准STL序列容器:vector、string、deque和list。 标准STL关联容器:set、multiset、map和multimap。 非标准序列容器slist和rope。slis......
条款2:小心对“容器无关代码”的幻想
STL是建立在泛化之上的。数组泛化为容器,参数化了所包含的对象的类型。函数泛化为算法,参数化了所用的迭代器的类型。指针泛化为迭代器,参数化了所指向的对象的类型。这只是个开始。独立的容器类型泛化为序列或关联容器,而且类似的容器拥有类似的功能。标准的内存相邻容器(参见条款1)都提供随机访问迭代器,标准......
条款3:使容器里对象的拷贝操作轻量而正确
容器容纳了对象,但不是你给它们的那个对象。此外,当你从容器中获取一个对象时,你所得到的对象不是容器里的那个对象。取而代之的是,当你向容器中添加一个对象(比如通过insert或push_back等),进入容器的是你指定的对象的拷贝。拷进去,拷出来。这就是STL的方式。一旦一个对象进入一个容器,以后对......
条款7:当使用new得指针的容器时,记得在销毁容器前delete那些指针
STL中的容器非常优秀。它们提供了前向和逆向遍历的迭代器(通过begin、end、rbegin等);它们能告诉你所容纳的对象类型(通过value_type的typedef);在插入和删除中,它们负责任何需要的内存管理;它们报告容纳了多少对象和最多可能容纳的数量(分别通过size和max_size);......
条款8:永不建立auto_ptr的容器
坦白地说,本条款不需要出现在《Effective STL》里。auto_ptr的容器(COAPs)是禁止的。试图使用它们的代码都不能编译。C++ 标准委员会花费了无数努力来安排这种情况[1]。我本来不需要说有关COAPs的任何东西,因为你的编译器对这样的容器应该有很多抱怨,而且所有那些都是不能编译的......
条款9:在删除选项中仔细选择
假定你有一个标准STL容器,c,容纳int,Container c;而你想把c中所有值为1963的对象都去掉。令人吃惊的是,完成这项任务的方法因不同的容器类型而不同:没有一种方法是通用的。如果你有一个连续内存容器(vector、deque或string——参见条款1),最......
条款12:对STL容器线程安全性的期待现实一些
标准C++的世界是相当保守和陈旧的。在这个纯洁的世界,所有可执行文件都是静态链接的。不存在内存映射文件和共享内存。没有窗口系统,没有网络,没有数据库,没有其他进程。在这种情况下,当发现标准没有提到任何关于线程的东西时你不该感到惊讶。你对STL的线程安全有的第一个想法应该是它将因实现而不同。当然,多......
条款20:为指针的关联容器指定比较类型
假定你有一个string*指针的set,你把一些动物的名字插入进set:set ssp; // ssp = “set of string ptrs”ssp.insert(new string("Anteater......
条款23:考虑用有序vector代替关联容器
当需要一个提供快速查找的数据结构时,很多STL程序员立刻会想到标准关联容器:set、multiset、map和multimap。直到现在这很好,但不是永远都好。如果查找速度真得很重要,的确也值得考虑使用非标准的散列容器(参见条款25)。如果使用了合适的散列函数,则可以认为散列容器提供了常数时间的查找......
条款25:熟悉非标准散列容器
STL程序员一般用不了多久就开始惊讶,“vector、list、map,很好,但是散列(hash)表在哪里”?唉,在标准C++库里没有任何散列表。 每个人都同意这是个不幸,但是标准委员会觉得需要加给他们的工作可能会过渡地推迟标准的完成。可以肯定标准的下一个版本将包含散列表,但是目前,STL没有散列的......
条款30:确保目标区间足够大
STL容器在被添加时(通过insert、push_front、push_back等)自动扩展它们自己来容纳新对象。这工作的很好,有些程序员因为这个信仰而被麻痹,认为他们不必担心要为容器中的对象腾出空间,因为容器自己可以照顾好这些。如果是那样就好了!当程序员想向容器中插入对象但并没有告诉STL他们所想......
条款33:提防在指针的容器上使用类似remove的算法
在管理一堆动态分配的Widgets,每一个都可能通过检验,你把结果指针保存在一个vector中:class Widget{public: ... bool isCertified() const; // 这个Widget是否通过检验 ...};vector&l......
取消
感谢您的支持,我会继续努力的!
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦

您的支持,是我们前进的动力!