
前言std::transform定义在algorithm作用是把一个区间里的元素逐个经过函数处理后写到另一个区间。它是映射map操作在 STL 里的对应物常见于批量转换全部平方、全部转大写、两个数组逐元素相加。常见误解是transform会自动扩容目标容器。不会。它只接收输出迭代器写出多少个元素由输入区间长度决定。如果目标容器是空的还直接传dst.begin()就是往没有元素的内存里写属于 UB。正确做法是提前resize或者用std::back_inserter。另一个容易忽略的点是二元版本的第二输入区间。标准只要求它至少有第一区间那么长不做边界检查短了就是越界读长了则多余部分被忽略。本文讲一元与二元两种形式、原地变换、输出方式以及和手写循环的取舍。一、一元变换基本形式transform(first, last, result, unary_op)对[first, last)中每个元素调用unary_op把返回值依次写到从result开始的区间返回写完后的输出迭代器位置。// 适用于 C17#include algorithm#include iostream#include vectorint main(){const std::vectorint src{1, 2, 3, 4};std::vectorint dst;dst.reserve(src.size());std::transform(src.begin(), src.end(), std::back_inserter(dst),[](int x) { return x * x; });for (int v : dst) {std::cout v ;}std::cout \n;return 0;}std::back_inserter是iterator里的插入迭代器每次赋值等价于push_back。它会按需扩容适合目标容器初始为空的情况。reserve不是必需的但能减少多次扩容。unary_op的返回类型决定了目标元素的类型。如果目标容器是std::vectordouble而 Lambda 返回int会发生隐式转换如果类型不兼容编译失败。二、二元变换二元版本接收两个输入区间和一个输出迭代器transform(first1, last1, first2, result, binary_op)它从first1走到last1同时从first2开始取同样多的元素逐个调用binary_op。// 适用于 C17#include algorithm#include iostream#include vectorint main(){const std::vectorint a{1, 2, 3};const std::vectorint b{10, 20, 30};std::vectorint sum(a.size());std::transform(a.begin(), a.end(), b.begin(), sum.begin(),[](int x, int y) { return x y; });for (int v : sum) {std::cout v ;}std::cout \n;return 0;}sum预先构造为a.size()个元素所以sum.begin()是合法的写入位置。关键约束b至少要有a.size()个元素。标准不检查这一点b短了就会读到区间外属于 UB。调用前应自己确认长度或者用std::min截断。三、原地变换把输出迭代器写成输入区间的起点就是原地变换// 适用于 C17#include algorithm#include cctype#include iostream#include stringint main(){std::string text Hello, World;std::transform(text.begin(), text.end(), text.begin(),[](unsigned char c) {return static_castchar(std::toupper(c));});std::cout text \n;return 0;}这里有两个细节。第一std::toupper的参数是int如果直接传char当char是有符号类型且值为负比如某些非 ASCII 字节时行为是 UB。先转成unsigned char再传才是标准保证安全的写法。第二原地变换要求输出位置与输入位置一一对应且unary_op不依赖其他元素。如果变换需要读邻居比如差分、卷积原地写会覆盖还没读的数据结果就错了。四、输出方式的三种选择方式写法适用风险预先 resizedst.resize(n); transform(..., dst.begin(), ...)已知输出长度长度算错会越界back_insertertransform(..., std::back_inserter(dst), ...)目标为空、逐个追加可能多次扩容原地transform(begin, end, begin, ...)元素类型不变、逐元素独立依赖邻居时会读脏数据如果输出长度与输入相同且目标容器已经分配好直接写begin()最快。如果目标容器为空back_inserter最省心。五、与手写循环的比较std::transform本质上就是一个for循环// 适用于 C17for (std::size_t i 0; i src.size(); i) {dst[i] src[i] * src[i];}两者的性能在开启优化后通常接近具体取决于编译器能否内联 Lambda、能否向量化。不要断言transform一定更快或更慢这类结论需要针对具体代码测量。transform的优势是可读性和复用意图明确不容易写错下标配合执行策略C17 的std::execution::par还能请求并行执行但并行版本需要额外支持且不保证调用顺序。六、顺序保证标准对非并行版本的transform通常按顺序处理元素但不要依赖unary_op的调用顺序来实现有状态逻辑。如果 Lambda 捕获了外部计数器并依赖调用次序代码会变得脆弱换一个标准库实现或开启并行就可能改变行为。需要顺序时老老实实写循环。七、变换到不同类型transform不要求输入输出元素类型相同。下面的例子把一组字符串映射为它们的长度// 适用于 C17#include algorithm#include cstddef#include string#include vectorstd::vectorstd::size_t lengthsOf(const std::vectorstd::string words){std::vectorstd::size_t lengths;lengths.reserve(words.size());std::transform(words.begin(), words.end(),std::back_inserter(lengths),[](const std::string s) { return s.size(); });return lengths;}Lambda 的返回类型是std::string::size_type与std::size_t在常见实现上一致。如果目标容器元素类型更窄比如int会发生隐式转换超长字符串的长度可能截断这类转换要自己确认范围。反过来从数字变换成字符串也可以只需让 Lambda 返回std::string。这时目标容器用back_inserter最方便因为每个元素的长度不再固定。常见坑点❌ 目标容器为空却传dst.begin()。✅ 先resize或用std::back_inserter(dst)。❌ 二元版本第二区间比第一区间短。✅ 调用前保证b.size() a.size()否则越界读UB。❌ 原地变换时 Lambda 读取相邻元素。✅ 前面元素已被覆盖需要邻居就输出到新容器。❌ 直接对char调用std::toupper。✅ 先转unsigned char避免负值传入的 UB。❌ 忘记包含algorithm只靠其他头文件间接引入。✅ 显式包含所用算法和iterator用back_inserter时。❌ Lambda 在不同分支返回不同类型导致推导冲突。✅ 统一返回类型或显式写返回类型。❌ 对std::map的键做原地变换。✅ 键是 const不能修改需要改键只能重建容器。❌ 认为并行执行策略下元素仍按顺序处理。✅ 并行版本不保证顺序有状态 Lambda 会出错。总结形式签名要点输出一元(first, last, result, op)每个输入对应一个输出二元(first1, last1, first2, result, op)两个输入区间逐元素合并原地result first元素类型不变时可用std::transform的用法只有一句话给定输入区间和输出位置逐元素套一个函数。真正要小心的是输出空间够不够、第二区间够不够长、原地写会不会覆盖还没读的数据。这三点确认无误剩下的就是选一个清楚的 Lambda。