【问题标题】:How exactly remove_if() works?remove_if() 究竟是如何工作的?
【发布时间】:2021-08-15 00:01:39
【问题描述】:

我参考了很多资源,但无法理解它究竟是如何修改容器中的元素的部分。 我写了一些代码试图理解,谁能解释发生了什么?

#include<bits/stdc++.h>
using namespace std;

bool test(char c)
{
    return (c=='a');
}

int isPalindrome(string A) 
{
    remove_if(A.begin(), A.end(), test);
    cout<<A<<'\n';   
 }

int main() 
{
    
    string A="aaaaabbbbb";
    isPalindrome(A); 

    return 0;
}

根据我目前的理解,它应该将所有 a 字符移动到 b 的右侧,如果我们尝试打印它,我们应该得到

bbbbbaaaaa

相反,我得到了

bbbbbbbbbb

【问题讨论】:

  • remove_if 应该完全删除它们.. 不要移动它们
  • 发生的事情是预期的行为。它根据谓词检查每个元素,如果条件为真,它们将被删除
  • 在输出中打印额外 b 的原因是什么? @妮娜
  • 算法不可能直接使用迭代器来修改容器。 std::remove_if 可以做的最好的事情是对容器进行分区,使剩余的元素保留在分区的一侧,而容器的其余部分处于某种未指定的状态。该函数返回一个迭代器,它定义了有效值范围的结束。然后由函数的用户通过实际删除该迭代器之后的所有内容来缩小容器。
  • @Scheff -- 这是一个有趣的引用,因为它是错误的。 std::remove 和 std:remove_if 对序列进行操作。该标准没有直接对序列结束后的任何内容施加任何要求。效果很可能是这些元素被移出,因此将产生从它们移出的任何结果。是的,当序列来自容器时,容器的大小不会改变。那是因为算法不是在容器上运行,而是在序列上运行。

标签: c++ stl c++17


【解决方案1】:

如果需要,该算法只是将等于“b”的元素移动或复制(取决于使用的迭代器)到存储等于“a”的元素的位置。

它不会交换容器的元素。

【讨论】:

  • 回复:“它不会交换容器的元素。”事实上,它并不知道也不关心这个特定的 sequence 是由容器提供的。它对序列进行操作。
【解决方案2】:

这是std::remove_if,而不是std::move_if。它只是从test 函数中删除所有返回true 的元素。现在,为什么要多出bs?好吧,你不应该看到他们。 std::remove_if 返回一个标记序列新结尾的迭代器,您应该使用返回值来相应地调整容器的大小。所以假设我们有一个字符串"Hello, World!",我们需要从中删除所有l。下面是一个示例程序,展示了这样做的一种方法:

#include <algorithm>
#include <string>
#include <iostream>

int main()
{
    std::string a = "Hello, World!";
    a.resize(std::distance(
        a.begin(),
        std::remove_if(a.begin(), a.end(), [](char c){return c == 'l';})
    ));
    std::cout << a << '\n';
}

输出:

Heo, Word!

【讨论】:

  • 惯用的方式是使用a.erase(std::remove_if(...), std::end(a))。您的方法有效,但并非每个容器都有resize。 erase 方法适用于所有没有固定大小的容器。
  • @tarunmali 它被称为 lambda,基本上是一个匿名函数,您可以将其放入其他函数中。随意对它进行一些研究以了解更多信息。到那时,你可以看看这个程序,它和我在我的答案中得到的一样,但使用普通函数而不是 lambda:godbolt.org/z/va68s9jMn
  • @tarunmali 见Lambda expressions
  • @tarunmali 我不确定会有很大的不同。 resize 也可以按照 erase 来实现。如果有任何差异,那将是微不足道的,这将取决于实际的实现。
  • @tarunmali 刚刚做了resize、.erase 和std::erase_if 的基准测试,因为我很好奇,根据quick-bench (GNU libstdc++ & GCC 10.2)、.erase 和std::erase_if 大致相同,而resize 显然快了 1.1 倍。我仍然可能会使用std::erase_if,因为它看起来更直观,如果不是这样,那么可能.erase 与不支持resize 的其他容器保持一致,并且因为它似乎是惯例而不是resize .
【解决方案3】:

remove_if 的工作方式是正确的。但是,它返回一个迭代器,该迭代器指向第一个被删除的元素,这与最后一个未被删除的元素相同。然后,您需要删除已删除的元素以获得您想要的答案。因此,在函数isPalindrome 中,将您的remove_if 行替换为

   const auto new_end = remove_if(A.begin(), A.end(), test);
   A.erase(new_end, A.end());

然后你会得到bbbbb。

这种调用remove_if 后跟erase 的模式经常发生。这两行通常合二为一,称为“remove-erase idiom”。在您的情况下,将您的 remove_if 行替换为

   A.erase( remove_if(A.begin(), A.end(), test), A.end() );

【讨论】:

  • 不,返回的迭代器没有指向第一个被移除的元素。移除的元素已被覆盖;它们不再存在。
  • 我的错。它应该只是“但是,它返回一个迭代器,该迭代器指向最后一个未删除的元素。”
【解决方案4】:

根据我目前的理解,它应该将所有 a 字符移到 b 的右侧

几乎。它做的相反:它(有点)将 b 字符移动到 a 的左侧。更准确地说,它在移除的元素上“移动分配”。

因此,左边的元素是从右边移动的b(在左边的a之上),右边的元素是移动的b到左侧。由于移动 char 与复制相同,即它不会修改被移动的参数,因此左右元素都是 b。

由于该算法的预期用例是删除 a's,因此容器中没有a's 不是问题。请注意,对于不同的输入,一些 a 可能会保留在容器的右端。右边的元素要么是被移动到左边的元素的残余(不是 a's),要么是因为要被移除而没有移动的元素(a's)。

附:代替 GregReese 的 answer 和 François Andrieux 的 comment 中显示的先前惯用模式,您现在可以在 C++20(或将来,假设您已标记 C++17)中使用:

std::erase_if(A, test);

【讨论】:

    【解决方案5】:

    我在几个 cmets 中都提出了相同的观点,但我认为需要更详细地解决它。

    像std::remove 和std::remove_if 这样的算法适用于序列,即由一对迭代器指定的元素。第一个迭代器指向序列的第一个元素,第二个迭代器指向元素序列的末尾。实际上,算法会移动或复制序列后面的元素,以覆盖正在被移除的元素。

    请注意,上一段没有使用容器这个词。容器是管理一系列元素的一种方式,但它不是唯一的方式。算法对容器一无所知。 remove 和 remove_if 无法更改他们一无所知的容器的大小。

    在将容器的元素传递给remove 或remove_if 后查看容器时看到的是修改后的序列(从容器的开头到算法返回的迭代器指向的元素),其次是剩下的。对于像char 这样的简单类型,这个残基可能是开始时的任何东西。对于具有非平凡移动赋值运算符的更复杂类型,残差由移动赋值留下的任何内容组成。对于 C++ 标准库中定义的类型,这意味着残基由处于未指定但有效状态的对象组成。对于在标准库以外的地方定义的类型,它们将处于设计者选择的任何状态。

    【讨论】:

      猜你喜欢
      • 2011-06-26
      • 2012-06-08
      • 2011-10-11
      • 2013-07-05
      • 1970-01-01
      • 1970-01-01
      • 2014-09-29
      • 2011-06-01
      • 1970-01-01
      相关资源
      最近更新 更多