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

条款9:在删除选项中仔细选择

假定你有一个标准STL容器,c,容纳int,

Container<int> c;

而你想把c中所有值为1963的对象都去掉。令人吃惊的是,完成这项任务的方法因不同的容器类型而不同:没有一种方法是通用的。

如果你有一个连续内存容器(vector、deque或string——参见条款1),最好的方法是erase-remove惯用法(参见条款32):

c.erase(remove(c.begin(), c.end(), 1963),        // 当c是vector、string
        c.end());                // 或deque时,
                        // erase-remove惯用法
                        // 是去除特定值的元素
                        // 的最佳方法

这方法也适合于list,但是,正如条款44解释的,list的成员函数remove更高效:

c.remove(1963);        // 当c是list时,
            // remove成员函数是去除
            // 特定值的元素的最佳方法

当c是标准关联容器(即,set、multiset、map或multimap)时,使用任何叫做remove的东西都是完全错误的。这样的容器没有叫做remove的成员函数,而且使用remove算法可能覆盖容器值(参见条款32),潜在地破坏容器。(关于这样的破坏的细节,参考条款22,那个条款也解释了为什么试图在map和multimap上使用remove肯定不能编译,而试图在set和multiset上使用可能不能编译。)

不,对于关联容器,解决问题的适当方法是调用erase:

c.erase(1963);        // 当c是标准关联容器时
            // erase成员函数是去除
            // 特定值的元素的最佳方法

这不仅是正确的,而且很高效,只花费对数时间。(序列容器的基于删除的技术需要线性时间。)并且,关联容器的erase成员函数有基于等价而不是相等的优势,条款19解释了这一区别的重要性。

让我们现在稍微修改一下这个问题。不是从c中除去每个有特定值的物体,让我们消除下面判断式(参见条款39)返回真的每个对象:

bool badValue(int x);    // 返回x是否是“bad”

对于序列容器(vector、string、deque和list),我们要做的只是把每个remove替换为remove_if,然后就完成了:

c.erase(remove_if(c.begin(), c.end(), badValue),    // 当c是vector、string
            c.end());            // 或deque时这是去掉
                        // badValue返回真
                        // 的对象的最佳方法
c.remove_if(badValue);                // 当c是list时这是去掉
                        // badValue返回真
                        // 的对象的最佳方法

对于标准关联容器,它不是很直截了当。有两种方法处理该问题,一个更容易编码,另一个更高效。“更容易但效率较低”的解决方案用remove_copy_if把我们需要的值拷贝到一个新容器中,然后把原容器的内容和新的交换:

AssocContainer<int> c;                // c现在是一种
...                        // 标准关联容器
AssocContainer<int> goodValues;            // 用于容纳不删除
                        // 的值的临时容器
remove_copy_if(c.begin(), c.end(),            // 从c拷贝不删除
        inserter(goodValues,        // 的值到
            goodValues.end()),        // goodValues
            badValue);
c.swap(goodValues);                // 交换c和goodValues
                        // 的内容

对这种方法的缺点是它拷贝了所有不删除的元素,而这样的拷贝开销可能大于我们感兴趣支付的。

我们可以通过直接从原容器删除元素来避开那笔帐单。不过,因为关联容器没有提供类似remove_if的成员函数,所以我们必须写一个循环来迭代c中的元素,和原来一样删除元素。

看起来,这个任务很简单,而且实际上,代码也很简单。不幸的是,那些正确工作的代码很少是跃出脑海的代码。例如,这是很多程序员首先想到的:

AssocContainer<int> c;
...
for (AssocContainer<int>::iterator i = c.begin();    // 清晰,直截了当
        i!= c.end();            // 而漏洞百出的用于
        ++i) {                // 删除c中badValue返回真
    if (badValue(*i)) c.erase(i);        // 的每个元素的代码
}                        // 不要这么做!

唉,这有未定义的行为。当容器的一个元素被删时,指向那个元素的所有迭代器都失效了。当c.erase(i)返回时,i已经失效。那对于这个循环是个坏消息,因为在erase返回后,i通过for循环的++i部分自增。

为了避免这个问题,我们必须保证在调用erase之前就得到了c中下一元素的迭代器。最容易的方法是当我们调用时在i上使用后置递增:

AssocContainer<int> c;
...
for (AssocContainer<int>::iterator i = c.begin();    // for循环的第三部分
    i != c.end();                // 是空的;i现在在下面
    /*nothing*/ ){                // 自增
    if (badValue(*i)) c.erase(i++);        // 对于坏的值,把当前的
    else ++i;                    // i传给erase,然后
}                        // 作为副作用增加i;
                        // 对于好的值,
                        // 只增加i

这种调用erase的解决方法可以工作,因为表达式i++的值是i的旧值,但作为副作用,i增加了。因此,我们把i的旧值(没增加的)传给erase,但在erase开始执行前i已经自增了。那正好是我们想要的。正如我所说的,代码很简单,只不过不是大多数程序员在第一次尝试时想到的。

现在让我们进一步修改该问题。不仅删除badValue返回真的每个元素,而且每当一个元素被删掉时,我们也想把一条消息写到日志文件中。

对于关联容器,这说多容易就有多容易,因为只需要对我们刚才开发的循环做一个微不足道的修改就行了:

ofstream logFile;                    // 要写入的日志文件
AssocContainer<int> c;
...
for (AssocContainer<int>::iterator i = c.begin();    // 循环条件和前面一样
    i !=c.end();){
    if (badValue(*i)){ 
        logFile << "Erasing " << *i <<'\n';    // 写日志文件 
        c.erase(i++);            // 删除元素
    }
    else ++i;
}

现在是vector、string和deque给我们带来麻烦。我们不能再使用erase-remove惯用法,因为没有办法让erase或remove写日志文件。而且,我们不能使用刚刚为关联容器开发的循环,因为它为vector、string和deque产生未定义的行为!要记得对于那样的容器,调用erase不仅使所有指向被删元素的迭代器失效,也使被删元素之后的所有迭代器失效。在我们的情况里,那包括所有i之后的迭代器。我们写i++,++i或你能想起的其它任何东西都没有用,因为没有能导致迭代器有效的。

我们必须对vector、string和deque采用不同的战略。特别是,我们必须利用erase的返回值。那个返回值正是我们需要的:一旦删除完成,它就是指向紧接在被删元素之后的元素的有效迭代器。换句话说,我们这么写:

for (SeqContainer<int>::iterator i = c.begin(); 
    i != c.end();){
    if (badValue(*i)){
        logFile << "Erasing " << *i << '\n'; 
        i = c.erase(i);            // 通过把erase的返回值
    }                    // 赋给i来保持i有效
    else
        ++i;
}

这可以很好地工作,但只用于标准序列容器。由于论证一个可能的问题(条款5做了),标准关联容器的erase的返回类型是void[1]。对于那些容器,你必须使用“后置递增你要传给erase的迭代器”技术。(顺便说说,在为序列容器编码和为关联容器编码之间的这种差别是为什么写容器无关代码一般缺乏考虑的一个例子——参见条款2。)

为了避免你奇怪list的适当方法是什么,事实表明对于迭代和删除,你可以像vector/string/deque一样或像关联容器一样对待list;两种方法都可以为list工作。

如果我们观察在本条款中提到的所有东西,我们得出下列结论:

  • 去除一个容器中有特定值的所有对象
    如果容器是vector、string或deque,使用erase-remove惯用法。
    如果容器是list,使用list::remove。
    如果容器是标准关联容器,使用它的erase成员函数。

  • 去除一个容器中满足一个特定判定式的所有对象
    如果容器是vector、string或deque,使用erase-remove_if惯用法。
    如果容器是list,使用list::remove_if。
    如果容器是标准关联容器,使用remove_copy_if和swap,或写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。

  • 在循环内做某些事情(除了删除对象之外)
    如果容器是标准序列容器,写一个循环来遍历容器元素,每当调用erase时记得都用它的返回值更新你的迭代器。
    如果容器是标准关联容器,写一个循环来遍历容器元素,当你把迭代器传给erase时记得后置递增它。

如你所见,与仅仅调用erase相比,有效地删除容器元素有更多的东西。解决问题的最好方法取决于你是怎样鉴别出哪个对象是要被去掉的,储存它们的容器的类型,和当你删除它们的时候你还想要做什么(如果有的话)。只要你小心而且注意了本条款的建议,你将毫不费力。如果你不小心,你将冒着产生不必要低效的代码或未定义行为的危险。

插入辅文

条款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......
取消
感谢您的支持,我会继续努力的!
扫码支持
扫码打赏,你说多少就多少

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

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