层级 | 核心知识点 |
---|---|
入门 | 基本语法、与for_each 对比、单/双范围操作 |
进阶 | 动态扩展、原地转换、类型兼容性、异常安全 |
高阶 | 性能优化、C++20 Ranges、transform_if 模拟 |
一、入门
1、描述std::transform
的基本功能,并写出两种版本的函数原型
std::transform
函数是 C++ 标准库<algorithm>
头文件中的一个算法,其主要用途是对一个或两个范围的元素进行变换 ,并将结果存储到另一个范围中。它有两种重载形式:一种用于单范围变换,另一种用于双范围变换。
一元版本:处理单个输入范围,函数原型:
cpp
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
:一元操作符,用于对输入范围的每个元素进行变换。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> input = {1, 2, 3, 4, 5};
std::vector<int> output(input.size());
// 将一个整数向量中的每个元素乘以 2
std::transform(input.begin(), input.end(), output.begin(), [](int x) {
return x * 2;
});
return 0;
}
二元版本:处理两个输入范围,函数原型:
cpp
template< class InputIt1, class InputIt2, class OutputIt, class BinaryOperation >
OutputIt transform( InputIt1 first1, InputIt1 last1,
InputIt2 first2, OutputIt d_first, BinaryOperation binary_op );
first1
和last1
:定义第一个输入范围的迭代器。first2
:第二个输入范围的起始迭代器,其长度应至少与第一个输入范围相同。d_first
:输出范围的起始迭代器,用于存储变换后的结果。binary_op
:二元操作符,用于对两个输入范围的对应元素进行变换。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> input1 = {1, 2, 3, 4, 5};
std::vector<int> input2 = {5, 4, 3, 2, 1};
std::vector<int> output(input1.size());
std::transform(input1.begin(), input1.end(),
input2.begin(), output.begin(), [](int x, int y)
{
return x + y;
});
return 0;
}
2、与for_each
的区别
-
std::for_each
:主要用于对一个范围内的每个元素执行某种操作,重点在于操作本身,通常这些操作会产生副作用,比如修改元素、打印日志等。该函数不返回经过操作后的数据,它的返回值类型是传入的可调用对象类型,且这个可调用对象的返回值类型通常为void
。 -
std::transform
:专注于对一个或多个范围内的元素进行转换,通过返回值生成一个新的序列。它可以将转换后的结果输出到不同的容器中,常用于无副作用的纯转换操作。
cpp
template< class InputIt, class UnaryFunction >
UnaryFunction for_each( InputIt first, InputIt last, UnaryFunction f );
// first 和 last:定义输入范围的迭代器。
// f:一元可调用对象,对输入范围内的每个元素执行该操作。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
// 定义一个用于 for_each 的可调用对象,用于打印元素并将元素加 1
struct PrintAndIncrement {
void operator()(int& num) {
std::cout << "Original: " << num << std::endl;
num += 1;
std::cout << "After increment: " << num << std::endl;
}
};
// 定义一个用于 transform 的可调用对象,用于将元素乘以 2
int MultiplyByTwo(int num) {
return num * 2;
}
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 使用 std::for_each 进行带副作用的操作
std::for_each(numbers.begin(), numbers.end(), PrintAndIncrement());
// 重置 numbers 向量
numbers = {1, 2, 3, 4, 5};
std::vector<int> result(numbers.size());
// 使用 std::transform 进行纯转换操作
std::transform(numbers.begin(), numbers.end(), result.begin(), MultiplyByTwo);
return 0;
}
二、进阶
1、如何在不预分配目标容器空间时使用transform
?
在使用 std::transform
时,如果不提前为目标容器分配空间,直接使用普通的迭代器可能会导致未定义行为,因为迭代器可能会访问到容器外部的内存。
而使用 std::back_inserter
可以解决这个问题。std::back_inserter
是一个插入迭代器,它会在每次赋值操作时调用目标容器的 push_back
方法,这样就可以动态地向容器中添加元素,而不需要提前分配空间。
cpp
std::vector<int> results;
std::transform(src.begin(), src.end(),
std::back_inserter(results), [](int x) { return x * 2; });
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
int main() {
// 定义源容器
std::vector<int> src = {1, 2, 3, 4, 5};
// 定义目标容器,初始为空,不预分配空间
std::vector<int> results;
// 使用 std::transform 和 std::back_inserter
std::transform(src.begin(), src.end(),
std::back_inserter(results), [](int x) {
return x * 2;
});
return 0;
}
std::back_inserter(results)
作为输出迭代器,它会在每次赋值操作时调用results
容器的push_back
方法,将转换后的元素添加到results
容器中。
2、如何用transform
实现原地修改?可能存在哪些风险?
将目标迭代器设为输入范围的起始迭代器。
风险 :若操作抛出异常,容器可能处于部分修改状态,也需确保操作不破坏元素依赖关系。
cpp
std::transform(vec.begin(), vec.end(), vec.begin(), ::toupper); // 字符串转大写
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <cctype>
int main() {
std::vector<std::string> vec = {"hello", "world", "cpp"};
// 使用 std::transform 实现原地修改
for (auto& str : vec) {
std::transform(str.begin(), str.end(), str.begin(), [](unsigned char c) {
return std::toupper(c);
});
}
return 0;
}
3、ransform
是否支持输入与输出类型不同?举例说明
cpp
// 将std::string转换为哈希值:
std::vector<std::string> words {"one", "two"};
std::vector<size_t> hashes;
std::transform(words.begin(), words.end(),
std::back_inserter(hashes), std::hash<std::string>{});
std::transform
是支持输入与输出类型不同的。它的设计初衷是对一个或多个范围的元素进行转换操作,最终将结果存储到另一个范围中。在这个过程中,转换操作可以将输入类型的数据转换为不同类型的输出数据,只要提供合适的转换函数即可。
三、高阶
1、如何通过自定义函数对象优化transform
性能?
- 使用无状态的函数对象(如
struct
重载operator()
),便于编译器内联优化 - 避免在
lambda
中捕获大型对象,减少拷贝开销。
- 无状态的函数对象是指不包含任何成员变量的函数对象。
- 编译器在处理无状态函数对象时,更容易进行内联优化。
- 内联优化是指编译器将函数调用替换为函数体本身,从而减少函数调用的开销。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
// 无状态的函数对象,用于计算平方
struct Square {
int operator()(int x) const {
return x * x;
}
};
int main() {
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dest(src.size());
// 使用自定义的无状态函数对象进行转换
std::transform(src.begin(), src.end(), dest.begin(), Square{});
return 0;
}
当使用 lambda 表达式作为转换操作时,如果捕获了大型对象,会产生拷贝开销。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
// 大型对象示例
struct LargeObject {
int data[1000];
};
int main() {
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dest(src.size());
// 避免捕获大型对象
LargeObject largeObj;
// 使用不捕获大型对象的 lambda
std::transform(src.begin(), src.end(), dest.begin(), [](int x) {
return x * 2;
});
// 输出结果
for (int num : dest) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
- 在
std::transform
调用中,使用的 lambda 表达式没有捕获LargeObject
类型的对象,避免了拷贝开销。如果确实需要使用大型对象,可以考虑使用引用捕获,如[&largeObj](int x) { /* 使用 largeObj */ }
,但要注意引用的生命周期问题。
2、C++20的ranges::transform
有何改进?
C++20引入范围语法,简化迭代器传递。支持更简洁的链式操作(如与views
组合)。
cpp
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> src = {1, 2, 3, 4, 5};
std::vector<int> dest(src.size());
std::ranges::transform(src, dest.begin(), [](int x) {
return x * 2;
});
return 0;
}
cpp
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> src = {1, 2, 3, 4, 5};
// 链式操作:先过滤出偶数,再将其乘以 2
auto result = src | std::views::filter([](int x) { return x % 2 == 0; })
| std::views::transform([](int x) { return x * 2; });
return 0;
}
std::views::filter
是一个视图适配器,用于过滤出满足条件的元素。std::views::transform
是另一个视图适配器,用于对元素进行转换。- 通过使用管道操作符
|
,可以将多个视图适配器组合在一起,形成一个链式操作。整个操作是惰性计算的,只有在遍历result
范围时才会实际执行过滤和转换操作,避免了创建中间容器,提高了性能和代码的简洁性。
3、STL未提供transform_if
,如何模拟其功能?
法1:先转换再过滤
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
int main() {
std::vector<int> src = { -1, 2, -3, 4, -5 };
std::vector<int> filtered;
// 先进行转换:使用 std::transform 对 src 中的每个元素进行转换。如果元素大于 0,则将其乘以 2;否则,将其设置为 -1。
std::transform(src.begin(), src.end(), std::back_inserter(filtered),
[](int x) { return x > 0 ? x * 2 : -1; });
// 过滤掉不符合条件的元素:使用 std::remove 和 erase 组合来移除值为 -1 的元素
// std::remove 将值为 -1 的元素移到容器末尾,并返回一个指向新的逻辑末尾的迭代器,然后 erase 函数删除这些元素。
filtered.erase(std::remove(filtered.begin(), filtered.end(), -1), filtered.end());
return 0;
}
法2:结合std::copy_if
与transform
cpp
int main() {
std::vector<int> src = { -1, 2, -3, 4, -5 };
std::vector<int> temp;
std::vector<int> result;
// 过滤出符合条件的元素
std::copy_if(src.begin(), src.end(),
std::back_inserter(temp), [](int x) { return x > 0; });
// 对符合条件的元素进行转换
std::transform(temp.begin(), temp.end(),
std::back_inserter(result), [](int x) { return x * 2; });
return 0;
}
- 过滤操作 :使用
std::copy_if
将src
中大于 0 的元素复制到temp
容器中。 - 转换操作 :使用
std::transform
对temp
中的元素进行转换,将每个元素乘以 2
法3:使用std::accumulate
手动处理条件
cpp
#include <iostream>
#include <vector>
#include <numeric>
int main() {
std::vector<int> src = { -1, 2, -3, 4, -5 };
std::vector<int> result;
// 使用 std::accumulate 手动处理
std::accumulate(src.begin(), src.end(), std::back_inserter(result), [](auto& out, int x) {
if (x > 0) {
out = x * 2;
++out;
}
return out;
});
// 输出结果
for (int num : result) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
std::accumulate
会遍历 src
中的每个元素,根据条件判断是否进行转换。如果元素大于 0,则将其乘以 2 并添加到 result
容器中。
4、 在使用std::transform
时,如何处理可能出现的异常?请给出一个考虑异常安全性的示例
在使用std::transform
时,异常可能来自于传入的操作符(如 lambda 函数、函数对象等)。
为了处理可能出现的异常,我们可以在操作符内部进行异常处理,或者在调用std::transform
的地方进行异常捕获。
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <stdexcept>
std::string convert_string(const std::string& str) {
// convert_string函数用于将输入字符串转换为大写,如果输入字符串为空,则抛出std::invalid_argument异常
if (str.empty()) {
throw std::invalid_argument("Input string is empty");
}
std::string result = str;
for (char& c : result) {
c = std::toupper(c);
}
return result;
}
int main() {
std::vector<std::string> input = {"hello", "", "world"};
std::vector<std::string> output(input.size());
try {
std::transform(input.begin(), input.end(), output.begin(), convert_string);
for (const std::string& str : output) {
std::cout << str << " ";
}
std::cout << std::endl;
} catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
}
return 0;
}