前言
看到 remove 这个单词的第一反应是什么意思?我的第一感觉是删除、去掉的意思,就像一个程序员看到 string 就会说是字符串,而不会说它是线、或者细绳的意思,可是C++里居然有个函数叫 std::remove()
,调用完这个函数什么也没删除,这我就奇怪了,打开有道词典查询一下:
不查不要紧,一查吓一跳,以下是词典给出的三个释义:
- vt. 移动,迁移;开除;调动
- vi. 移动,迁移;搬家
- n. 移动;距离;搬家
及物动词、不及物动词、名词给出的含义都是移动,只有一个开除的意思和删除有点像,难道我穿越了?我之前一直以为它是删除的意思啊,很多函数还是用它命名的呢!
赶紧翻翻其他的字典,给高中的英语老师打个电话问问,最终还是在一些释义中找到了删除的意思,还有一些用作删除的例句,有趣的是在有道词典上,所有的单词解释都和移动有关,所有的例句都是和删除有关。
remove_if
的历史
为什么要查单词的 remove 的意思,当然是被它坑过了,本来想从 std::vector<T>
中删除指定的元素,考虑到迭代器失效的问题,放弃了循环遍历的复杂处理,选择直接使用算法函数 std::remove_if()
来进行删除,之前对于 std::remove()
和 std::remove_if()
有过简单的了解,不过记忆还是出现了偏差。
一直记得 std::remove()
函数调用之后需要再使用 erase()
函数处理下,忘记了 std::remove_if()
函数也要做相同的处理,于是在出现问题的时候一度怀疑这个函数的功能发生了变更,开始找这个函数历史迭代的版本,这里推荐一个网站 C++标准函数查询 - std::remove_if(),用来查询函数的定义、所在头文件和使用方法非常方便。
文档中有这样两句:
1) Removes all elements that are equal to value, using operator== to compare them.
3) Removes all elements for which predicate p returns true.
解释函数作用时用到的单词都是 remove ,你说神不神奇,这里应该都是取的移动的意思。
这两句话对应的函数声明应该是:
1 | template< class ForwardIt, class T > |
这两个函数后面都有相同的说明—— (until C++20)
,意思大概就是说这两个函数一直到 C++20
版本都存在,在我的印象中 std::remove_if()
函数比较新,最起码得比 std::remove()
函数年轻几岁,可是他们到底是哪个版本添加到c++标准的的呢?中途的功能有没有发生变更,继续回忆!
第一次看到这两个函数应该是在看《Effective STL》这本书的时候,大概是5年前了,正好这个本书就放在手边,赶紧翻目录查一下,打开对应章节发现其中确实提到了删除 std::vector<T>
中的元素时,在调用了这两个函数之后都需要再调用 erase()
函数对待删除的元素进行擦除。
看看书的出版时间是2013年,难道是 C++11
的标准加上的,不对,看一下翻译者写得序,落款时间2003年,不能是 C++03
的标准吧?不过这是一本翻译书籍,再看看原作者 Scott Meyers
写的前言,落款时间2001年,好吧,看来这两个函数肯定在 C++98
的版本中就已经存在了,我有点惊呆了,这确实颠覆了我的记忆和认知。
造成这种认知错误主要有两方面原因,第一方面就是受到了开发环境的限制,从一开始学习的时候Turob C 2.0
、VC++ 6.0
、VS2005
、VS2008
、VS2010
就很少接触 C++11
的知识,Dev-C++
和 Code::Blocks
也是在特定的情况下使用,没有过多的研究,结果在刚开始工作的时候开发工具居然是VS2003
,这个版本我之前都没听说过,还好一步步升级到了08、13、17版本。
第二方面就是这两个函数常常与 Lambda
表达式,auto
关键字一起用,这都是 C++11
里才有的,让人感觉好像这个 std::remove_if()
函数也是 C++11
版本中的内容,造成了错觉。总来说还是用的少,不熟悉,以后多看多练就好了。
remove_if
的实现
要想更深入的学习 std::remove_if()
函数, 那这个函数实现的细节有必要了解一下,这有助于我们理解函数的使用方法,下面给出两个版本可能的实现方式,也许下面的实现与你查到的不一样,但是思想是相通的,有些实现细节中使用了 std::find_if()
函数,这里没有列举这个版本,下面这两个版本的代码更容易让人明白,它究竟做了哪些事情。
1 | // C++98 版本 |
1 | // C++11 版本 |
对比两段代码有没有发现区别——只改了半行代码,将赋值语句中的 *first
在 C++11
版本中替换成了 std::move(*first)
,这只能发生在 C++11
之后,因为 std::move()
函数是 C++11
才加入的。
这代码乍一看挺唬人的,其实仔细分析一下还挺简单的,只是这些符号看起来有些生疏,其实可以把 ForwardIterator
看成一个指针类型,UnaryPredicate
是一个函数类型,我们改写一下:
1 | int* remove_if (int* first, int* last, func_type func) |
这代码是不是就比较接地气了,想象一下,一个是包含10个元素的数组,让你删除其中的偶数怎么做?其实就是遍历一遍数组,从开始位置到结束位置逐个判断,如果不是偶数就不进行操作,如果是偶数就把当前的偶数向前移动到结果指针上就好了,结果指针向后移动准备接受下一个奇数,这个判断是不是偶数的函数就是上面代码中的 func()
。
最后结果指针 result
停在有效元素后面一个位置上,这个位置到结尾指针 last
的位置上的元素都应该被删除,这就是为什么常常将 std::remove_if()
函数的返回值作为 erase()
函数的第一个参数,而将 last
指针作为 erase()
函数的第二个参数,实际作用就是将这些位置上的元素擦除,从头擦到尾,达到真正删除的目的。
具体使用
说了这么多,接下来看看具体怎么用,我们将 std::remove_if()
函数和 erase()
函数分开使用,主要看一下调用 std::remove_if()
函数之后的 vector
中元素的值是怎么变的。
1 |
|
运行结果为:
0 1 2 3 4 5 6 7 8 9
1 3 5 7 9 5 6 7 8 9
1 3 5 7 9
从结果可以看出,第二步调用 std::remove_if()
函数之后,vector 中的元素个数并没有减少,只是将后面不需要删除的元素移动到了 vector 的前面,从第二行结果来看,调用 std::remove_if()
函数之后返回的结果 itor
指向5,所以擦除从5所在位置到结尾的元素就达到了我们的目的。
这段代码在 C++98
、C++11
、C++14
环境下都可以编译运行,在这里推荐一个在线编译器 C++ Shell,可以测试各个版本编译器下运行结果,界面简洁明了,方便测试。
上面的代码其实写得有些啰嗦,如果使用 C++11
语法之后,可以简写为:
1 | , |
运行结果:
0 1 2 3 4 5 6 7 8 9
1 3 5 7 9
总结
- 对于模糊的知识要花时间复习,避免临时用到的时候手忙脚乱出问题
- 对于一些心存疑虑的函数可以看一下具体的实现,知道实现的细节可以让我们更加清楚程序都做了哪些事情
- 对于新的技术标准可以不精通,但是必须花一些时间进行了解,比如新的
C++
标准 - 对于违反常识的代码,先不要否定,即使在你的运行环境中报错,说不定人家是新语法呢?
- 曾经看到一段在类的定义时初始化非静态变量的代码,一度认为编译不过,但后来发现在
C++11
中运行的很好