没想到C++中的std::remove_if()函数历史还挺悠久

前言

看到 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
2
3
4
5
template< class ForwardIt, class T >
ForwardIt remove( ForwardIt first, ForwardIt last, const T& value ); // (until C++20)

template< class ForwardIt, class UnaryPredicate >
ForwardIt remove_if( ForwardIt first, ForwardIt last, UnaryPredicate p ); // (until C++20)

这两个函数后面都有相同的说明—— (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.0VC++ 6.0VS2005VS2008VS2010就很少接触 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// C++98 版本
template <class ForwardIterator, class UnaryPredicate>
ForwardIterator remove_if (ForwardIterator first, ForwardIterator last,
UnaryPredicate pred)
{
ForwardIterator result = first;
while (first!=last) {
if (!pred(*first)) {
*result = *first;
++result;
}
++first;
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// C++11     版本
template <class ForwardIterator, class UnaryPredicate>
ForwardIterator remove_if (ForwardIterator first, ForwardIterator last,
UnaryPredicate pred)
{
ForwardIterator result = first;
while (first!=last) {
if (!pred(*first)) {
*result = std::move(*first);
++result;
}
++first;
}
return result;
}

对比两段代码有没有发现区别——只改了半行代码,将赋值语句中的 *firstC++11 版本中替换成了 std::move(*first),这只能发生在 C++11 之后,因为 std::move() 函数是 C++11 才加入的。

这代码乍一看挺唬人的,其实仔细分析一下还挺简单的,只是这些符号看起来有些生疏,其实可以把 ForwardIterator 看成一个指针类型,UnaryPredicate 是一个函数类型,我们改写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
int* remove_if (int* first, int* last, func_type func)
{
int* result = first;
for (;first!=last;++first)
{
if (!func(*first))
{
*result = *first;
++result;
}
}
return result;
}

这代码是不是就比较接地气了,想象一下,一个是包含10个元素的数组,让你删除其中的偶数怎么做?其实就是遍历一遍数组,从开始位置到结束位置逐个判断,如果不是偶数就不进行操作,如果是偶数就把当前的偶数向前移动到结果指针上就好了,结果指针向后移动准备接受下一个奇数,这个判断是不是偶数的函数就是上面代码中的 func()

最后结果指针 result 停在有效元素后面一个位置上,这个位置到结尾指针 last 的位置上的元素都应该被删除,这就是为什么常常将 std::remove_if() 函数的返回值作为 erase() 函数的第一个参数,而将 last 指针作为 erase() 函数的第二个参数,实际作用就是将这些位置上的元素擦除,从头擦到尾,达到真正删除的目的。

具体使用

说了这么多,接下来看看具体怎么用,我们将 std::remove_if() 函数和 erase() 函数分开使用,主要看一下调用 std::remove_if() 函数之后的 vector 中元素的值是怎么变的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <iostream>
#include <vector>
#include <algorithm>

bool isEven(int n) // 是否是偶数
{
return n % 2 == 0;
}

int main()
{
std::vector<int> vecTest;
for (int i = 0; i < 10; ++i)
vecTest.push_back(i);

for (int i = 0; i < vecTest.size(); ++i)
std::cout << vecTest[i] << " ";
std::cout << std::endl;

// 移动元素
std::vector<int>::iterator itor = std::remove_if(vecTest.begin(), vecTest.end(), isEven);

// 查看移动后的变化
for (int i = 0; i < vecTest.size(); ++i)
std::cout << vecTest[i] << " ";
std::cout << std::endl;

// 删除元素
vecTest.erase(itor, vecTest.end());

for (int i = 0; i < vecTest.size(); ++i)
std::cout << vecTest[i] << " ";

return 0;
}

运行结果为:

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++98C++11C++14 环境下都可以编译运行,在这里推荐一个在线编译器 C++ Shell,可以测试各个版本编译器下运行结果,界面简洁明了,方便测试。

上面的代码其实写得有些啰嗦,如果使用 C++11 语法之后,可以简写为:

1

运行结果:

0 1 2 3 4 5 6 7 8 9
1 3 5 7 9

总结

  1. 对于模糊的知识要花时间复习,避免临时用到的时候手忙脚乱出问题
  2. 对于一些心存疑虑的函数可以看一下具体的实现,知道实现的细节可以让我们更加清楚程序都做了哪些事情
  3. 对于新的技术标准可以不精通,但是必须花一些时间进行了解,比如新的 C++ 标准
  4. 对于违反常识的代码,先不要否定,即使在你的运行环境中报错,说不定人家是新语法呢?
  5. 曾经看到一段在类的定义时初始化非静态变量的代码,一度认为编译不过,但后来发现在 C++11 中运行的很好
Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客