[C++面试] 你了解transform吗?

层级 核心知识点
入门 基本语法、与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 );
  • first1last1:定义输入范围的迭代器,指定要变换的元素范围。
  • 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 );
  • first1last1:定义第一个输入范围的迭代器。
  • 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_iftransform

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_ifsrc 中大于 0 的元素复制到 temp 容器中。
  • 转换操作 :使用 std::transformtemp 中的元素进行转换,将每个元素乘以 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;
}
相关推荐
戴国进7 分钟前
全面讲解python的uiautomation包
开发语言·python
橘猫云计算机设计29 分钟前
基于Java的班级事务管理系统(源码+lw+部署文档+讲解),源码可白嫖!
java·开发语言·数据库·spring boot·微信小程序·小程序·毕业设计
叱咤少帅(少帅)39 分钟前
Go环境相关理解
linux·开发语言·golang
熬了夜的程序员42 分钟前
Go 语言封装邮件发送功能
开发语言·后端·golang·log4j
士别三日&&当刮目相看1 小时前
JAVA学习*String类
java·开发语言·学习
王嘉俊9251 小时前
ReentranLock手写
java·开发语言·javase
my_realmy1 小时前
JAVA 单调栈习题解析
java·开发语言
海晨忆1 小时前
JS—ES5与ES6:2分钟掌握ES5与ES6的区别
开发语言·javascript·es6·es5与es6的区别
高飞的Leo2 小时前
工厂方法模式
java·开发语言·工厂方法模式
菜鸡中的奋斗鸡→挣扎鸡2 小时前
c++ count方法
开发语言·c++