ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C++ STL算法进阶:从基础使用到高效组合与性能优化

C++ STL算法进阶:从基础使用到高效组合与性能优化 1. 从“会用”到“用好”STL算法的进阶之路如果你已经写过一些C代码对std::vector、std::map这些容器和std::sort、std::find这些基础算法不再陌生那么恭喜你你已经跨过了STLStandard Template Library的门槛。但很多时候我们仅仅停留在“能用”的层面——知道有这么个函数查一下文档把参数填进去编译通过功能跑通就心满意足了。然而STL算法的真正威力远不止于此。它更像是一个设计精密的瑞士军刀套装每一件工具都有其最趁手的应用场景和独特的设计哲学。停留在“查文档、填参数”的阶段就像拿着一把多功能钳子只会拧螺丝不仅效率低下还可能因为误用而“伤到自己”——写出性能低下、逻辑晦涩甚至暗藏bug的代码。我见过不少代码库充斥着手动实现的循环去完成本应由一个标准算法轻松搞定的事情或者因为对算法内部机制的不了解导致了意料之外的低效。比如在已排序的容器里依然使用std::find进行线性查找而不是std::binary_search或者在对std::list进行大量中间插入时却选择了std::vector。这些选择背后是对STL组件特性理解的缺失。进阶使用STL算法的核心在于从“语法正确”迈向“语义精准”和“性能最优”。你需要理解每个算法背后的假设比如迭代器类别要求、它的时间复杂度承诺、以及对数据状态的期望是否要求已排序。这不仅仅是记忆API更是培养一种“算法思维”——在面对一个具体问题时能迅速在脑海中映射出最合适的STL工具并清晰地知道为什么选它以及它可能带来的副作用。本文将聚焦于那些在日常开发中极具价值但可能容易被忽视或误用的STL算法。我们会绕过最基础的find、sort深入探讨如何组合使用算法、理解算法的谓词与投影、掌握那些专为有序区间设计的“神器”并剖析算法与容器特性的协同。目标是将你从STL的“用户”升级为“驾驭者”让你写出的C代码更简洁、更高效、也更优雅。2. 超越单一调用算法的组合与“管道”思维STL算法之所以强大一个重要原因是它们通过迭代器抽象与容器解耦这使得算法可以像乐高积木一样自由组合形成强大的数据处理“管道”。这种组合的核心模式是一个算法的输出区间作为另一个算法的输入区间。2.1 理解“结果写入迭代器”模式许多STL算法并不直接修改原始容器而是将结果输出到另一个由迭代器指定的位置。最典型的代表是std::copy、std::transform、std::remove_copy等。这类算法通常接受一个d_first迭代器指向目标范围的起始处。std::vectorint src {1, 2, 3, 4, 5}; std::vectorint dst; // 错误dst为空begin()迭代器无效会导致未定义行为 // std::copy(src.begin(), src.end(), dst.begin()); // 正确做法1使用std::back_inserter dst.clear(); std::copy(src.begin(), src.end(), std::back_inserter(dst)); // dst现在为 {1, 2, 3, 4, 5} // 正确做法2预先分配足够空间 dst.clear(); dst.resize(src.size()); // 关键步骤确保有足够容量 std::copy(src.begin(), src.end(), dst.begin());std::back_inserter是一个迭代器适配器它会对容器调用push_back。当你无法或不想预先确定目标大小时它是安全且方便的选择。但要注意频繁的push_back可能导致多次内存重新分配。如果最终大小可知预先reserve或resize再使用普通迭代器性能通常更优。2.2 构建数据处理流水线组合算法的经典场景是“过滤-转换”管道。例如我们有一个整数列表想先过滤掉所有奇数再将剩下的偶数乘以2。std::vectorint data {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; std::vectorint result; // 方法1分两步清晰但需要中间容器 std::vectorint evens; std::copy_if(data.begin(), data.end(), std::back_inserter(evens), [](int x) { return x % 2 0; }); std::transform(evens.begin(), evens.end(), std::back_inserter(result), [](int x) { return x * 2; }); // result: {4, 8, 12, 16, 20} // 方法2一步到位更高效但谓词逻辑稍复杂 result.clear(); std::transform(data.begin(), data.end(), std::back_inserter(result), [](int x) { if (x % 2 0) return x * 2; // 对于奇数我们需要一个“跳过”的标记。但transform必须为每个输入产生输出。 // 这暴露了简单组合的局限性。 return -1; // 引入无效值后续还需要过滤不理想。 });方法2的困境揭示了简单组合的不足std::transform必须为每个输入元素产生一个输出。对于这种“条件转换”的需求C20引入了ranges库和std::views可以优雅地实现惰性求值的管道// C20 方式 (需要编译器支持) #include ranges auto view data | std::views::filter([](int x){ return x % 2 0; }) | std::views::transform([](int x){ return x * 2; }); // view是一个惰性视图此时并未进行计算 std::vectorint result(view.begin(), view.end()); // 触发计算在C20之前我们可以利用std::copy_if结合一个“转换谓词”但需要一点技巧通常需要定义一个状态复杂的函数对象或者退回两步法。这里的经验是当算法组合变得笨拙时往往是重新审视问题或期待新语言特性的信号。在C17及之前清晰的两步法通常比绞尽脑汁的一步“奇技淫巧”更可维护。2.3std::remove与erase的经典组合真正理解“删除”这是STL初学者最容易踩坑的地方之一。std::remove和std::remove_if并不直接删除容器中的元素std::vectorint v {1, 2, 3, 2, 5, 2, 7}; auto new_end std::remove(v.begin(), v.end(), 2); // 此时 v 的内容可能是 {1, 3, 5, 7, ?, ?, ?} // ‘?‘ 表示未被定义的值通常是原来位置的元素但已被移走或覆盖 // v.size() 仍然是 7 // new_end 指向第一个“不需要保留”的元素的位置即逻辑新序列的尾后 // 为了真正删除元素必须结合容器的erase方法 v.erase(new_end, v.end()); // 这才是真正的删除 // 现在 v {1, 3, 5, 7}, size() 4std::remove的工作方式是遍历区间将所有“需要保留”的元素移动到区间的前部并返回一个指向“新逻辑末尾”的迭代器。它通过赋值来“移动”元素因此对于非平凡类型它可能调用大量的赋值运算符。对于std::list应优先使用成员函数list.remove()和list.remove_if()它们的效率更高通过操作链表指针。这个组合如此常用以至于它被称为“Erase–remove idiom”。对于顺序容器记住这个固定搭配// 删除所有值为val的元素 container.erase(std::remove(container.begin(), container.end(), val), container.end()); // 删除所有满足条件的元素 container.erase(std::remove_if(container.begin(), container.end(), predicate), container.end());3. 谓词与投影定制算法行为的双翼算法泛化的能力很大程度上来自于谓词Predicate和投影Projection C20引入概念但C17已有相关实践这两个抽象。它们允许你将自定义逻辑注入到标准算法中。3.1 谓词不仅仅是bool返回类型谓词是一个可调用对象接受一定数量的参数并返回一个能在布尔上下文中使用的值。它不仅是true/false的判断器更是算法比较逻辑的定制器。二元谓词在排序中的应用struct Task { int priority; std::string name; }; std::vectorTask tasks {{2, Fix bug}, {1, Write doc}, {3, Review code}}; // 按priority升序排序 std::sort(tasks.begin(), tasks.end(), [](const Task a, const Task b) { return a.priority b.priority; }); // 按name字典序排序 std::sort(tasks.begin(), tasks.end(), [](const Task a, const Task b) { return a.name b.name; }); // 先按priority降序再按name升序 std::sort(tasks.begin(), tasks.end(), [](const Task a, const Task b) { if (a.priority ! b.priority) return a.priority b.priority; // 降序 return a.name b.name; });谓词的严格弱序要求对于std::sort,std::lower_bound,std::set的排序准则等谓词必须满足“严格弱序”。简单来说它需要满足非自反性comp(a, a)必须为false。非对称性若comp(a, b)为true则comp(b, a)必须为false。可传递性若comp(a, b)为true且comp(b, c)为true则comp(a, c)必须为true。等价传递性如果!comp(a,b) !comp(b,a)即a和b“等价”并且!comp(b,c) !comp(c,b)那么必须有!comp(a,c) !comp(c,a)。违反这些规则例如在比较函数中使用了而不是会导致未定义行为程序可能崩溃或产生错误结果。注意Lambda表达式是生成谓词最方便的方式但要注意捕获列表。如果谓词需要依赖外部状态需谨慎处理生命周期和线程安全。3.2 投影在比较前先“转换”数据投影是C20 ranges库正式引入的概念但它反映了一种常见模式在应用算法特别是比较之前先对元素进行某种“映射”或“提取”。在C17及之前我们通过在谓词中手动实现投影std::vectorPerson people {{25, Alice}, {30, Bob}, {20, Charlie}}; // 按年龄排序需要在每个比较中访问成员 std::sort(people.begin(), people.end(), [](const Person a, const Person b) { return a.age b.age; });在C20中投影使得意图更清晰并可能避免重复的成员访问逻辑// C20 std::ranges::sort(people, {}, Person::age); // 第三个参数是投影按成员 age 排序 // 等价于按 people[i].age 进行比较投影不仅限于成员指针也可以是任何一元可调用对象// 按名字长度排序 std::ranges::sort(people, std::ranges::less{}, [](const Person p) { return p.name.length(); });投影的实用价值在于解耦它将“如何获取比较键”与“如何比较键”分离开。谓词专注于比较逻辑如std::less投影专注于键的提取。这使得代码更容易复用和组合。即使在C20之前有意识地采用这种“先提取键再比较”的思维也能写出更清晰的谓词。4. 有序区间的算法效率提升的关键STL为已排序的区间提供了一组特殊的算法。它们的共同前提是输入区间必须至少按照算法所使用的比较准则进行排序。如果这个前提不满足结果将是未定义的。这些算法通常具有O(log n)或更好的时间复杂度远优于线性算法。4.1 查找算法binary_search,lower_bound,upper_bound,equal_range这是最常被混淆的一组算法。std::binary_search: 只告诉你元素是否存在返回bool。它不告诉你位置。std::vectorint v {1, 3, 5, 7, 9}; bool found std::binary_search(v.begin(), v.end(), 5); // true bool not_found std::binary_search(v.begin(), v.end(), 4); // falsestd::lower_bound: 返回第一个不小于即大于或等于给定值的元素迭代器。如果值存在它指向该值的第一个出现位置如果不存在它指向第一个大于该值的位置即插入该值后仍能保持顺序的位置。std::vectorint v {1, 2, 2, 3, 4}; auto it_low std::lower_bound(v.begin(), v.end(), 2); // *it_low 2, 指向第一个2 it_low std::lower_bound(v.begin(), v.end(), 5); // it_low v.end()因为所有元素都小于5std::upper_bound: 返回第一个大于给定值的元素迭代器。std::vectorint v {1, 2, 2, 3, 4}; auto it_up std::upper_bound(v.begin(), v.end(), 2); // *it_up 3, 指向第一个大于2的元素std::equal_range: 返回一个迭代器对[first, last)表示等于给定值的元素范围。它本质上等价于std::make_pair(lower_bound(...), upper_bound(...))但可能更高效只进行一次二分查找。auto range std::equal_range(v.begin(), v.end(), 2); // range.first 指向第一个2, range.second 指向3 // 遍历这个范围就得到了所有等于2的元素 for (auto it range.first; it ! range.second; it) { std::cout *it ; // 输出: 2 2 }选择指南只关心是否存在用binary_search。想找到插入位置或第一个不小于目标的值用lower_bound。想找到大于目标的值用upper_bound。想获取所有等于目标值的元素范围用equal_range。4.2 集合操作算法includes,set_union,set_intersection,set_difference,set_symmetric_difference这些算法模拟了数学集合操作要求两个输入区间都已排序且输出区间不能与输入区间重叠除非特别说明。std::includes: 判断一个已排序序列是否包含另一个已排序序列即是否为子集。std::vectorint superset {1, 2, 3, 4, 5, 6}; std::vectorint subset {2, 4, 6}; bool contains std::includes(superset.begin(), superset.end(), subset.begin(), subset.end()); // truestd::set_union: 求并集输出两个序列中的所有元素重复元素只取一次。std::set_intersection: 求交集输出同时存在于两个序列中的元素。std::set_difference: 求差集 (A - B)输出在第一个序列中但不在第二个序列中的元素。std::set_symmetric_difference: 求对称差集输出只存在于其中一个序列中的元素即并集减去交集。这些算法都是输出到指定的迭代器通常需要配合back_inserter或预先分配空间的容器使用。std::vectorint v1 {1, 2, 3, 4, 5}; std::vectorint v2 {3, 4, 5, 6, 7}; std::vectorint result_union, result_intersection; std::set_union(v1.begin(), v1.end(), v2.begin(), v2.end(), std::back_inserter(result_union)); // result_union: {1, 2, 3, 4, 5, 6, 7} std::set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), std::back_inserter(result_intersection)); // result_intersection: {3, 4, 5}重要提示这些算法默认使用进行比较。如果你的序列是按自定义谓词排序的必须在调用算法时传入相同的谓词。4.3 合并算法merge与inplace_mergestd::merge: 将两个已排序的序列合并成一个新的有序序列。它是set_union的“多副本”版本——如果元素在两个输入中都存在merge会输出两份而set_union只输出一份。std::vectorint v1 {1, 3, 5}; std::vectorint v2 {2, 4, 6}; std::vectorint dst; std::merge(v1.begin(), v1.end(), v2.begin(), v2.end(), std::back_inserter(dst)); // dst: {1, 2, 3, 4, 5, 6}std::inplace_merge: 将一个序列中两个相邻的已排序子序列进行原地合并。这在实现归并排序等算法时非常有用。std::vectorint v {1, 3, 5, 2, 4, 6}; // 前半部分[0,3)已排序后半部分[3,6)已排序 std::inplace_merge(v.begin(), v.begin() 3, v.end()); // v: {1, 2, 3, 4, 5, 6}5. 数值算法与内存操作容易被低估的实用工具除了常见的查找、排序、修改序列的算法STL还提供了一些用于数值计算和底层内存操作的算法它们在某些场景下能极大简化代码。5.1 数值算法accumulate,inner_product,partial_sum,adjacent_difference这些算法定义在numeric头文件中。std::accumulate: 经典的“折叠”或“reduce”操作。计算区间内所有元素的累积值默认为求和。它接受一个初始值和一个可选的二元操作符。std::vectorint v {1, 2, 3, 4, 5}; int sum std::accumulate(v.begin(), v.end(), 0); // 求和初始值0 // sum 15 int product std::accumulate(v.begin(), v.end(), 1, std::multipliesint()); // product 120 (1*2*3*4*5) // 连接字符串 std::vectorstd::string words {Hello, , World, !}; std::string sentence std::accumulate(words.begin(), words.end(), std::string()); // sentence Hello World!注意初始值的类型它决定了整个运算的类型。如果对int容器求和初始值用0.0double结果会是double。这在处理浮点数时很重要。std::inner_product: 计算两个序列的内积点积。同样可以自定义“加法”和“乘法”操作。std::vectorint a {1, 2, 3}; std::vectorint b {4, 5, 6}; int dot std::inner_product(a.begin(), a.end(), b.begin(), 0); // dot 1*4 2*5 3*6 32std::partial_sum: 计算前缀和或自定义二元操作的前缀结果。std::vectorint v {1, 2, 3, 4, 5}; std::vectorint prefix_sums; std::partial_sum(v.begin(), v.end(), std::back_inserter(prefix_sums)); // prefix_sums: {1, 3, 6, 10, 15}std::adjacent_difference: 计算相邻元素的差值或自定义二元操作的结果。std::vectorint v {1, 3, 6, 10, 15}; std::vectorint diffs; std::adjacent_difference(v.begin(), v.end(), std::back_inserter(diffs)); // diffs: {1, 2, 3, 4, 5} (第一个元素是原第一个元素本身)5.2 内存算法fill,generate,iota,uninitialized_*系列这些算法直接操作内存有时比循环更清晰也可能被编译器更好地优化。std::fill/std::fill_n: 将区间内所有元素赋为指定值。std::vectorint v(10); std::fill(v.begin(), v.end(), 42); // 全部赋值为42 std::fill_n(v.begin(), 5, -1); // 前5个元素赋值为-1std::generate/std::generate_n: 通过调用一个生成器函数来为每个元素赋值。std::vectorint v(5); int n 0; std::generate(v.begin(), v.end(), [n]() { return n; }); // v: {0, 1, 2, 3, 4}std::iota: 用连续递增的值填充区间C11。这个名字来源于APL语言中的⍳函数。std::vectorint v(5); std::iota(v.begin(), v.end(), 10); // 从10开始递增 // v: {10, 11, 12, 13, 14}uninitialized_*系列如std::uninitialized_copy,std::uninitialized_fill这些算法用于在未初始化的内存上构造对象通常与自定义内存分配如placement new一起使用是编写容器或低级内存管理代码时的工具。日常业务代码中较少直接使用。6. 迭代器适配器与算法威力倍增器迭代器是STL算法的通用接口。除了容器提供的迭代器还有一些特殊的“迭代器适配器”它们能改变迭代器的行为从而与算法配合产生强大的效果。6.1 插入迭代器back_inserter,front_inserter,inserter如前所述std::back_inserter是最常用的它调用容器的push_back。std::front_inserter调用push_front因此只适用于有push_front的容器如std::list,std::deque。std::inserter则在指定位置前插入调用容器的insert方法。std::listint lst {1, 2, 3}; std::vectorint vec {4, 5, 6}; // 将vec的内容插入到lst的开头 std::copy(vec.begin(), vec.end(), std::front_inserter(lst)); // lst: {6, 5, 4, 1, 2, 3} (注意顺序是反的因为每次都在前端插入) // 在lst的第二个元素值为5之前插入vec的内容 auto it std::next(lst.begin()); // 指向第二个元素 std::copy(vec.begin(), vec.end(), std::inserter(lst, it)); // lst: {6, 4, 5, 6, 5, 4, 1, 2, 3} (在6和5之间插入了{4,5,6})6.2 流迭代器istream_iterator,ostream_iterator它们允许将输入/输出流当作序列来处理极大地简化了IO与算法的结合。std::istream_iterator: 从输入流读取数据。#include iterator #include sstream std::stringstream ss(1 2 3 4 5); std::istream_iteratorint input_start(ss); std::istream_iteratorint input_end; // 默认构造表示流结束 std::vectorint numbers(input_start, input_end); // 直接从流构造vector // numbers: {1, 2, 3, 4, 5}std::ostream_iterator: 向输出流写入数据。std::vectorint v {10, 20, 30}; std::copy(v.begin(), v.end(), std::ostream_iteratorint(std::cout, , )); // 输出: 10, 20, 30, // 注意末尾多了一个逗号和空格这是它的一个小缺点。6.3 反向迭代器reverse_iterator反向迭代器允许你从后向前遍历容器。rbegin()返回指向最后一个元素的逆向迭代器rend()返回指向第一个元素前一个位置的逆向迭代器。std::vectorint v {1, 2, 3, 4, 5}; // 逆序输出 std::copy(v.rbegin(), v.rend(), std::ostream_iteratorint(std::cout, )); // 输出: 5 4 3 2 1 // 在逆序查找时特别有用 auto it std::find(v.rbegin(), v.rend(), 3); // 从后往前找第一个3 if (it ! v.rend()) { // it.base() 返回一个正向迭代器指向it所指向元素的下一个位置 std::cout Found at position (from start): std::distance(v.begin(), it.base()) - 1 std::endl; }.base()方法的陷阱反向迭代器rit与对应的正向迭代器it的关系是*(rit) *(it - 1)。即rit.base()指向rit所指元素的下一个位置。在插入/删除时需特别注意。7. 算法选择与性能考量从理论复杂度到实际影响知道有哪些算法只是第一步在具体场景中选择最合适的算法并理解其性能影响是进阶的关键。7.1 时间复杂度不是唯一指标大O复杂度O(n), O(log n), O(n²)是重要的理论指导但实际性能还受以下因素影响常数因子一个O(n)的算法如果常数项很大在小数据量时可能比O(log n)的算法慢。缓存友好性顺序访问如std::vector的遍历通常比随机访问如std::list的遍历快得多因为CPU缓存预取机制。内存分配涉及容器大小变化的操作如push_back导致重新分配成本很高。算法具体实现不同标准库实现如GCC的libstdc、Clang的libc对同一算法的优化可能不同。示例std::sortvsstd::list::sortstd::sort要求随机访问迭代器所以它不能用于std::list。它对std::vector、std::deque、普通数组等进行排序平均复杂度O(n log n)通常是快速排序、内省排序或归并排序的混合实现非常高效。std::list有自己的成员函数sort它通过归并排序实现复杂度也是O(n log n)。但由于list节点在内存中不连续缓存不友好且归并排序需要额外的空间或复杂的指针操作对于同样数量的元素对list排序通常比对vector排序慢一个数量级以上。因此如果需要对大量数据进行排序优先考虑使用std::vector。7.2 根据容器特性选择算法std::vector/std::deque/std::array支持随机访问是所有STL算法的主场。优先使用标准算法。注意vector中间插入/删除是O(n)的。std::list/std::forward_list只支持双向或前向迭代器。许多需要随机访问迭代器的算法如std::sort不能用。但它们有高效的O(1)时间复杂度的插入和删除给定迭代器位置。对于这类容器使用成员函数版本的算法list.sort(),list.merge(),list.unique(),list.remove(),list.remove_if(),list.reverse()。这些算法专门为链表优化过。避免频繁调用std::advance(it, n)因为它是O(n)操作。std::set/std::map(及其无序版本)它们本身就是有序或哈希数据结构。查找自有find成员函数O(log n)或平均O(1)比通用算法std::findO(n)快得多。对于关联容器几乎总是使用其成员函数而不是STL算法。7.3 避免不必要的拷贝和计算使用引用和移动语义在自定义谓词或投影中如果对象较大尽量使用const 。对于即将消亡的对象考虑使用移动语义。std::vectorBigObject vec; // 不好的谓词按值捕获导致拷贝 std::sort(vec.begin(), vec.end(), [](BigObject a, BigObject b) { return a.key b.key; }); // 好的谓词按常量引用捕获 std::sort(vec.begin(), vec.end(), [](const BigObject a, const BigObject b) { return a.key b.key; });惰性求值与视图C20C20 Ranges库提供的视图如filter,transform是惰性的它们不立即产生新容器而是在迭代时动态计算。这可以避免中间容器的创建和拷贝在处理大数据或复杂管道时性能优势明显。// C20 之前可能产生多个中间vector auto result filter(transform(source, func1), predicate); // C20惰性视图无中间拷贝 auto view source | std::views::transform(func1) | std::views::filter(predicate);8. 实战中的经验与陷阱最后分享一些从实际项目中总结的经验和容易踩的坑。8.1 迭代器失效算法操作中的隐形炸弹许多算法会修改容器导致指向容器元素的迭代器、指针或引用失效。这是一个必须时刻警惕的问题。典型场景在循环中删除元素std::vectorint v {1, 2, 3, 4, 5}; // 错误删除元素后迭代器it及其后的迭代器都失效了 for (auto it v.begin(); it ! v.end(); it) { if (*it % 2 0) { v.erase(it); // 删除后it失效后续的it行为未定义 } } // 正确利用erase的返回值返回被删除元素之后元素的迭代器 for (auto it v.begin(); it ! v.end(); ) { if (*it % 2 0) { it v.erase(it); // erase返回新的有效迭代器 } else { it; } } // 更简洁的正确做法使用erase-remove idiom v.erase(std::remove_if(v.begin(), v.end(), [](int x) { return x % 2 0; }), v.end());std::remove系列算法不导致迭代器失效错std::remove通过移动元素来覆盖“被删除”的元素它返回的“新逻辑末尾”迭代器是有效的。但原来指向被移动元素位置的迭代器、指针、引用现在指向的是什么是已经被移走或覆盖的对象其值是不确定的。所以在调用std::remove后除了它返回的迭代器你不应再依赖容器中其他位置的旧迭代器。8.2 谓词的无状态要求STL算法通常不保证谓词被调用的次数、顺序或是否被拷贝。因此谓词最好是无状态的纯函数。如果谓词需要维护状态必须非常小心。// 一个危险的例子试图用谓词记录调用次数 struct BadPredicate { int count 0; bool operator()(int x) { count; // 修改状态 return x % 2 0; } }; std::vectorint v {1, 2, 3, 4, 5}; BadPredicate pred; v.erase(std::remove_if(v.begin(), v.end(), pred), v.end()); // pred.count 的值是多少不确定算法可能拷贝了谓词对象。如果需要状态应该通过引用捕获外部变量并注意线程安全或者使用std::reference_wrapper来传递谓词。8.3 算法与并行执行C17/20从C17开始许多STL算法有了并行版本接受一个执行策略std::execution::par等作为第一个参数。#include execution std::vectorint v {...}; // 非常大的vector // 顺序执行 std::sort(v.begin(), v.end()); // 并行执行可能利用多核 std::sort(std::execution::par, v.begin(), v.end());使用并行算法的注意事项谓词和操作必须线程安全不能有数据竞争。迭代器操作必须无副作用例如在并行for_each中修改其他元素是危险的。性能不一定提升对于小数据量并行开销可能抵消收益。并行算法引入了不确定性如元素处理顺序。异常处理更复杂如果并行执行中抛出异常行为与顺序版本不同。在决定使用并行算法前最好进行性能测试。8.4 自定义类型的算法支持要让自定义类型能很好地与STL算法协作需要提供适当的接口。排序和查找需要定义operator或者提供自定义比较谓词。哈希容器unordered_set,unordered_map需要提供哈希函数特化std::hash或自定义和相等比较operator或自定义谓词。流迭代器需要重载operator和operator。一个良好的实践是为你自定义的、有逻辑顺序的类型提供operator并为无序容器提供特化的std::hash。STL算法的深度远不止于此C20 Ranges库更是带来了革命性的变化让算法组合更加声明式和高效。但掌握本文所探讨的这些进阶内容已经足以让你在绝大多数场景下游刃有余写出既正确又高效的C代码。核心思想始终是理解工具的设计意图和约束根据具体问题选择最合适的工具并清楚知道为什么这个选择是最佳的。这需要实践和积累但一旦形成这种思维习惯你的代码质量将会有质的飞跃。
返回列表