C++ / a working model

87 / 103   ·   C++11   ·   约 12 分钟

过滤序列:算法重排与容器删除分开理解

先记住这句话

算法只看到迭代器范围,不负责改变容器大小。以订单状态过滤为例,区分复制到新序列、稳定划分和删除尾区间,并观察空输入、全部淘汰以及原顺序保留的契约。

本篇内容
  1. 需求先于容器技巧
  2. 逻辑终点不等于容器终点
  3. 现代简写不取代理解契约
  4. 运行示例
  5. 动手练习
READING EVIDENCE / 已读完整正文

Accelerated C++: Practical Programming by Example

连续读完第0—16章与附录A、B的全部可用正文、代码、Details与练习。另直接读完PDF中24幅实质图示,覆盖中位数、删除/划分、指针、复制、引用计数和图片继承图;非把提取完成算作阅读。完整范围指此公开第二印副本,不包括未附独立习题解答;练习未执行。个别转录与早期印次错误保留勘误说明。

查看版本、实际阅读范围与原文入口 →

需求先于容器技巧

要从一串任务中去掉取消项,先问是否要保留原输入、是否保留幸存项的顺序、谁拥有结果。书中通过学生分类不断改变实现,展示同一个业务规则可以用不同容器和算法表达。接口契约没有变,成本和失效规则却会变。

本例把任务缩成整数,非负值为保留项。copy_if 写入另一个 vector,原序列完全不动;就地 remove_if 则把保留项稳定地搬到前部,适合允许修改原序列的场景。它们都使用同一谓词含义,不让存储细节决定业务规则。

逻辑终点不等于容器终点

remove_if 返回的是保留前缀的终点。调用后 size 尚未缩小,尾部元素仍然存在,但不能把其具体值解释成被删除项清单。随后调用容器 erase,才销毁尾部元素并更新大小;这是算法与容器各自承担一半职责的结果。

示例不检查尾部碰巧留下什么,而是检查 erase 之后的完整序列。空输入自然形成空区间,全部淘汰时逻辑终点等于 begin。不要先保留指向某个业务对象的迭代器,再假设重排和删除后它仍表示同一个任务:即便某个位置仍可访问,那个位置的值也可能已经换了。

现代简写不取代理解契约

C++20 的 std::erase_if(vector, predicate) 可以直接表达就地过滤;这里保留两步写法,是为了说明 remove 算法不拥有容器的结构。若要同时得到两组并保留组内顺序,可以复制到两个目标,或用 stable_partition 得到分界,再自行决定是否拆成两个容器。

原书是 C++98 时代作品,函数对象和迭代器协议仍有教学价值,但旧式适配器不应直接移植。lambda 让局部判断更直观,标准 copy_if 也已经提供;这并不意味着算法会替你检查输出范围,裸 begin 指向空目标时仍不能写入,示例通过 back_inserter 建立新元素。

容易答错的地方

  • remove_if 不改变 vector::size;省略 erase 会把无意义尾部当成有效记录继续处理。
  • copy_if 输出到空 vector 的 begin 是越界写入;用 back_inserter,或先创建足够多的目标元素。

运行一个例子

最低标准 C++11 · 完整程序 · 下载 .cpp

#include <algorithm>
#include <cassert>
#include <iterator>
#include <vector>
void discard_negative(std::vector<int>& tasks) {
    const auto end = std::remove_if(tasks.begin(), tasks.end(),
        [](int value) { return value < 0; });
    tasks.erase(end, tasks.end());
}
int main() {
    const std::vector<int> original{3, -1, 0, -2, 3};
    std::vector<int> selected;
    std::copy_if(original.begin(), original.end(), std::back_inserter(selected),
        [](int value) { return value >= 0; });
    const std::vector<int> expected{3, 0, 3};
    assert(selected == expected);
    auto modified = original;
    discard_negative(modified);
    assert(modified == expected && original[1] == -1);
    std::vector<int> empty;
    discard_negative(empty);
    assert(empty.empty());
    std::vector<int> rejected{-1, -2};
    discard_negative(rejected);
    assert(rejected.empty());
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread books-accelerated-cpp.cpp -o example && ./example

预期结果

预期:正常退出、无输出;所有 assert 通过。

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

改成删除偶数,保留奇数原有顺序,负奇数也应保留。

查看参考答案

谓词改成 value % 2 == 0,不能用 value % 2 == 1 判断所有奇数,因为负奇数的余数可以为-1。对 {-3,-2,0,1,4,5} 执行两步删除,结果应完整等于 {-3,1,5};原有先后关系不变,证明的是稳定保留而非排序。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录