当前位置: 首页 > news >正文

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

文章目录

  • 前言
  • `remove_if`的历史
  • `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.
  2. Removes all elements for which predicate p returns true.

解释函数作用时用到的单词都是 remove ,你说神不神奇,这里应该都是取的移动的意思。

这两句话对应的函数声明应该是:

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() 函数,这里没有列举这个版本,下面这两个版本的代码更容易让人明白,它究竟做了哪些事情。

// 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;
}
// 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 是一个函数类型,我们改写一下:

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 中元素的值是怎么变的。

#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 语法之后,可以简写为:

#include <iostream>
#include <vector>
#include <algorithm>

int main()
{
    std::vector<int> vecTest{0, 1, 2, 3, 4, 5, 6, 7 ,8, 9};

    std::for_each(vecTest.begin(), vecTest.end(), [](int n){ std::cout << n << " "; });
    std::cout << std::endl;

    // 移动后删除元素
    vecTest.erase(std::remove_if(vecTest.begin(), vecTest.end(),
        [](int n){ return n % 2 == 0; }), vecTest.end());

    std::for_each(vecTest.begin(), vecTest.end(), [](int n){ std::cout << n << " "; });

    return 0;
}

运行结果:

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

总结

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

相关文章:

  • git stash帮你在切换分支前暂存不想提交的修改
  • Win10通过带命令行的安全模式清除顽固的广告弹窗文件
  • C++11中的时间库std::chrono(引发关于时间的思考)
  • .bat批处理(九):替换带有等号=的字符串的子串
  • 简单聊聊C/C++中的左值和右值
  • C++11在左值引用的基础上增加右值引用
  • 汇编指令入门级整理
  • 使用c++filt命令还原C++编译后的函数名
  • 配置Beyond Compare 4作为git mergetool来解决git merge命令导致的文件冲突
  • git在回退版本时HEAD~和HEAD^的作用和区别
  • 对称加密、非对称加密、公钥、私钥究竟是个啥?
  • 认证、HTTPS、证书的基本含义
  • 码龄10年工作6年的搬砖小哥,最常访问的学习网站都在这里了
  • C++中的std::lower_bound()和std::upper_bound()函数
  • 根证书的应用和信任基础
  • 【前端学习】-粗谈选择器
  • axios 和 cookie 的那些事
  • DOM的那些事
  • hadoop入门学习教程--DKHadoop完整安装步骤
  • JavaScript设计模式之工厂模式
  • java架构面试锦集:开源框架+并发+数据结构+大企必备面试题
  • MobX
  • Node.js 新计划:使用 V8 snapshot 将启动速度提升 8 倍
  • nodejs:开发并发布一个nodejs包
  • spark本地环境的搭建到运行第一个spark程序
  • Travix是如何部署应用程序到Kubernetes上的
  • Webpack入门之遇到的那些坑,系列示例Demo
  • 动手做个聊天室,前端工程师百无聊赖的人生
  • 关于Java中分层中遇到的一些问题
  • 区块链分支循环
  • 我感觉这是史上最牛的防sql注入方法类
  • 深度学习之轻量级神经网络在TWS蓝牙音频处理器上的部署
  • Prometheus VS InfluxDB
  • ​猴子吃桃问题:每天都吃了前一天剩下的一半多一个。
  • # 飞书APP集成平台-数字化落地
  • #DBA杂记1
  • (4)事件处理——(7)简单事件(Simple events)
  • (Matlab)遗传算法优化的BP神经网络实现回归预测
  • (vue)el-checkbox 实现展示区分 label 和 value(展示值与选中获取值需不同)
  • (附源码)springboot教学评价 毕业设计 641310
  • (转)Mysql的优化设置
  • .bashrc在哪里,alias妙用
  • .NET DevOps 接入指南 | 1. GitLab 安装
  • .php结尾的域名,【php】php正则截取url中域名后的内容
  • [ 代码审计篇 ] 代码审计案例详解(一) SQL注入代码审计案例
  • [2]十道算法题【Java实现】
  • [AR Foundation] 人脸检测的流程
  • [AX]AX2012开发新特性-禁止表或者表字段
  • [C++]模板与STL简介
  • [C语言]——函数递归
  • [Excel]如何找到非固定空白格數列的條件數據? 以月份報價表單為例
  • [fsevents@^2.1.2] optional install error: Package require os(darwin) not compatible with your platfo
  • [hive小技巧]同一份数据多种处理
  • [ICCV2017]Neural Person Search Machines
  • [LeetCode] 178. 分数排名