std::transform,一个非常有用的算法函数
一、简介
std::transform
是 C++ 标准库中的一个算法函数,位于 <algorithm>
头文件中。它允许对一个范围内的元素进行转换操作,并将结果存储到另一个范围中。
std::transform
的输入范围和输出范围的大小必须相等,否则行为是未定义的。同时,输出范围必须具有足够的空间来存储转换后的元素。
std::transform
的时间复杂度取决于输入范围的大小,通常为线性时间复杂度 O(N),其中 N 是输入范围的元素数量。但具体的性能表现还受到 unary_op
函数的执行时间和容器的特性影响。
二、在一个范围上 std:: transform
本质上,std::transform
对范围中的每个元素应用一个函数:
std::transform
的函数原型:
template <class InputIt, class OutputIt, class UnaryOperation>
OutputIt transform(InputIt first1, InputIt last1, OutputIt d_first, UnaryOperation unary_op);
参数说明:
first1
和last1
:输入范围的起始和结束迭代器。d_first
:输出范围的起始迭代器,用于存储转换后的结果。unary_op
:一元操作函数,用于对输入范围内的每个元素进行转换操作。
std::transform
对输入范围 [first1, last1)
中的每个元素调用 unary_op
函数,并将结果存储到从 d_first
开始的输出范围中。输出范围必须具有足够的空间来存储转换后的元素。
只要使用了STL,就会有std::transform
的需求。例如,要获取map
包含的键,可以使用std::transform
:
map<int, string> m = { {1,"foo"}, {42, "bar"}, {7, "baz"} };
vector<int> keys;
std::transform(m.begin(), m.end(), std::back_inserter(keys), getFirst);
其中getFirst
是需要用户实现的一个(非标准)函数,它接受一个pair
并返回它的第一个元素。上面使用的std::back_inserter
是一个输出迭代器,每次赋值给它时,它都会向传递给它的容器执行push_back
操作。这使程序员不必考虑输出的大小。
std::transform
的概念非常有用,因此它有一个来自函数式编程的名字:Map
(与std::map
无关)。实际上,可以反过来看这个问题:STL源于函数式编程,因此函数式编程中的一个核心概念在STL中占据核心地位也就不足为奇了。
三、在两个范围上 std:: transform
std::transform
还有一个重载版本,它接受两个范围作为输入,并对输入范围中的每个元素应用一个需要两个参数的函数。换句话说,它让你可以同时处理两个容器中相同位置的元素。
函数原型:
template<typename InputIterator1, typename InputIterator2, typename OutputIterator, typename BinaryOperation>
OutputIterator transform(InputIterator1 first1,
InputIterator1 last1,
InputIterator2 first2,
OutputIterator result,
BinaryOperation op);
但是,在使用此重载时需要小心,因为第二个范围至少需要与第一个范围一样长。
事实上,如图和原型所示,std::transform
遍历第一个范围,并从第二个范围读取对应值。但是它没有办法知道第二个范围实际上停止在哪里。这个重载使用了所谓的“1.5-Ranges”,因为第一个范围是完全提供的,但第二个范围没有结束部分。
示例,将两个整数范围的元素相加:
vector<int> numbers1 = {1, 5, 42, 7, 8};
vector<int> numbers2 = {10, 7, 4, 2, 2};
vector<int> results;
std::transform(numbers1.begin(), numbers1.end(),
numbers2.begin(),
std::back_inserter(results),
[](int i, int j) {return i+j;});
在两个范围上应用一个函数的概念也有一个来自函数式编程的名称:zip
。
三、原地 std::transform
输出范围可以是2个输入范围中的任何一个。在这种情况下,范围被“就地”转换。
原地std::transform
在一个范围上与std::for_each
有什么不同?其实,两者都对每个元素应用一个函数。
实际上有两个主要的区别,一个是技术上 的,在实践中相对不重要,另一个更重要:
- 不重要的技术问题:从标准的角度来看,
for_each
提供了比transform
更多的保证,即:- 从第一个元素到最后一个元素按顺序遍历该范围。
- 在遍历过程中不拷贝函数(或函数对象)。
- 因此,理论上可以通过
for_each
函数来控制函数对象的状态。但是一般来说,并不希望在函数对象中引入状态。
- 重要的一点是:
for_each
和transform
在处理元素时有不同的行为。即:for_each
会在每个元素上应用一个函数,但不会改变元素本身。transform
则会在每个元素上应用函数,并将结果赋值给相应的元素。- 简单来说,
for_each
只是对元素执行某个操作,而transform
不仅执行操作,还将操作的结果保存回原来的位置。
所以在有些情况下for_each更合适。例如,在更一般的意义上(IO输出,日志记录等),for_each应该优先考虑具有副作用,因为transform只是说…它转换您的元素。
因此,有些情况下更适合使用for_each
。例如,在更广义的层面上(如IO输出、日志记录等)需要产生副作用时,应首选for_each
,因为transform
只是表示……它会转换你的元素。
四、transform_if 概述
如果使用 std::transform
函数很多,有可能会遇到需要对某个范围内元素的特定部分应用变换的情况。这些元素将由一个谓词来标识。
因此,基于 std::copy_if
算法的模型,该算法只复制满足谓词的元素,首先想到的是应该有一个名为transform_if
的算法。但是,STL 中没有这样的算法,Boost 中也没有,其他任何地方也没有。
这本身就暗示了这种算法可能不是上述需求的最佳解决方案,而且这种解决方案确实存在一些问题:
-
它将是一个做两件事的函数:过滤谓词和应用函数。
-
应该以什么顺序传递谓词和函数?在某些情况下(特别是在
bool
和int
相互隐式转换的情况下),以错误的顺序传递它们可以编译通过,但不会达到想要的目的。虽然这可以通过强类型来解决。 -
应该如何处理现有的转换?如何处理不满足谓词的元素?它们应该被保留吗?
因此,transform_if算法并不是解决这种需求的正确方法(否则是合法的)。一个优雅而强大的解决方案是使用范围:
因此,这种(合法的)需求并不适合使用 transform_if
算法来解决。一种优雅而强大的解决方案是使用范围(range):
v | filter(myPredicate) | transform(f)
range
可以做transform_if
想要做的事情,甚至更多。
五、总结
C++的std::transform
算法函数是一个强大的工具,它能够以极简的方式解决数据转换的问题。通过这个函数,可以在一行代码中完成各种数据转换操作,无论是简单的数值运算还是复杂的自定义逻辑。本文从介绍函数的基本用法开始,逐步展示了如何灵活运用std::transform
函数来实现不同类型的数据转换。