MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ STL 算法 transform 的自定义操作

2022-08-085.4k 阅读

C++ STL 算法 transform 的自定义操作

在 C++ 标准模板库(STL)中,transform 算法是一个非常强大且灵活的工具。它允许我们对一个范围内的元素执行某种操作,并将结果存储到另一个位置。transform 可以接受自定义操作,这使得它在各种复杂场景下都能发挥作用。

transform 函数概述

transform 函数有两个重载版本,定义在 <algorithm> 头文件中。

第一个版本:

template<class InputIt, class OutputIt, class UnaryOperation>
OutputIt transform(InputIt first1, InputIt last1, OutputIt d_first,
                   UnaryOperation unary_op);

这个版本接受一个输入范围 [first1, last1),对该范围内的每个元素应用一元操作 unary_op,并将结果存储到从 d_first 开始的输出范围中。返回值是输出范围中最后一个被写入元素的下一个位置的迭代器。

第二个版本:

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 开始,长度与第一个范围相同。对两个范围中对应位置的元素应用二元操作 binary_op,并将结果存储到从 d_first 开始的输出范围中。同样,返回值是输出范围中最后一个被写入元素的下一个位置的迭代器。

自定义一元操作

当我们使用 transform 的一元操作版本时,我们需要定义一个一元函数对象或者函数指针。一元函数对象是一个重载了 () 运算符的类,这样它的实例就可以像函数一样被调用。

示例 1:使用函数指针作为一元操作

#include <iostream>
#include <algorithm>
#include <vector>

// 定义一元函数
int square(int num) {
    return num * num;
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::vector<int> squared_numbers(numbers.size());

    // 使用 transform 和函数指针
    std::transform(numbers.begin(), numbers.end(), squared_numbers.begin(), square);

    for (int num : squared_numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这个例子中,我们定义了一个 square 函数,它接受一个整数并返回其平方。然后我们使用 std::transformnumbers 向量中的每个元素平方,并将结果存储到 squared_numbers 向量中。

示例 2:使用函数对象作为一元操作

#include <iostream>
#include <algorithm>
#include <vector>

// 定义函数对象
class Square {
public:
    int operator()(int num) const {
        return num * num;
    }
};

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::vector<int> squared_numbers(numbers.size());

    // 使用 transform 和函数对象
    std::transform(numbers.begin(), numbers.end(), squared_numbers.begin(), Square());

    for (int num : squared_numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

这里我们定义了一个 Square 函数对象,通过重载 () 运算符来实现平方操作。在 main 函数中,我们创建了一个 Square 对象,并将其作为参数传递给 transform 算法。

示例 3:使用 lambda 表达式作为一元操作

#include <iostream>
#include <algorithm>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::vector<int> squared_numbers(numbers.size());

    // 使用 transform 和 lambda 表达式
    std::transform(numbers.begin(), numbers.end(), squared_numbers.begin(), [](int num) {
        return num * num;
    });

    for (int num : squared_numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

Lambda 表达式提供了一种简洁的方式来定义匿名函数。在这个例子中,我们使用 lambda 表达式来定义平方操作,并将其传递给 transform

自定义二元操作

当使用 transform 的二元操作版本时,我们需要定义一个二元函数对象或者函数指针。二元函数对象是一个重载了 () 运算符,接受两个参数的类。

示例 4:使用函数指针作为二元操作

#include <iostream>
#include <algorithm>
#include <vector>

// 定义二元函数
int add(int a, int b) {
    return a + b;
}

int main() {
    std::vector<int> numbers1 = {1, 2, 3, 4, 5};
    std::vector<int> numbers2 = {5, 4, 3, 2, 1};
    std::vector<int> sum_numbers(numbers1.size());

    // 使用 transform 和函数指针
    std::transform(numbers1.begin(), numbers1.end(), numbers2.begin(), sum_numbers.begin(), add);

    for (int num : sum_numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这个例子中,我们定义了一个 add 函数,它接受两个整数并返回它们的和。transform 算法将 numbers1numbers2 向量中对应位置的元素相加,并将结果存储到 sum_numbers 向量中。

示例 5:使用函数对象作为二元操作

#include <iostream>
#include <algorithm>
#include <vector>

// 定义函数对象
class Add {
public:
    int operator()(int a, int b) const {
        return a + b;
    }
};

int main() {
    std::vector<int> numbers1 = {1, 2, 3, 4, 5};
    std::vector<int> numbers2 = {5, 4, 3, 2, 1};
    std::vector<int> sum_numbers(numbers1.size());

    // 使用 transform 和函数对象
    std::transform(numbers1.begin(), numbers1.end(), numbers2.begin(), sum_numbers.begin(), Add());

    for (int num : sum_numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

这里我们定义了一个 Add 函数对象,通过重载 () 运算符来实现加法操作。在 main 函数中,我们创建了一个 Add 对象,并将其作为参数传递给 transform 算法。

示例 6:使用 lambda 表达式作为二元操作

#include <iostream>
#include <algorithm>
#include <vector>

int main() {
    std::vector<int> numbers1 = {1, 2, 3, 4, 5};
    std::vector<int> numbers2 = {5, 4, 3, 2, 1};
    std::vector<int> sum_numbers(numbers1.size());

    // 使用 transform 和 lambda 表达式
    std::transform(numbers1.begin(), numbers1.end(), numbers2.begin(), sum_numbers.begin(), [](int a, int b) {
        return a + b;
    });

    for (int num : sum_numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

同样,我们使用 lambda 表达式来定义二元加法操作,并将其传递给 transform

自定义操作的复杂应用

  1. 使用自定义操作进行数据类型转换 假设我们有一个 std::vector<int>,我们想将其转换为 std::vector<std::string>,可以使用 transform 结合自定义操作。
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include <sstream>

// 将 int 转换为 string 的函数对象
class IntToString {
public:
    std::string operator()(int num) const {
        std::ostringstream oss;
        oss << num;
        return oss.str();
    }
};

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::vector<std::string> string_numbers(numbers.size());

    std::transform(numbers.begin(), numbers.end(), string_numbers.begin(), IntToString());

    for (const std::string& str : string_numbers) {
        std::cout << str << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这个例子中,IntToString 函数对象将整数转换为字符串。transform 算法遍历 numbers 向量,对每个整数应用 IntToString 操作,并将结果存储到 string_numbers 向量中。

  1. 结合自定义操作和容器适配器 我们可以使用 transform 与容器适配器(如 std::stackstd::queue)结合。例如,我们有一个 std::stack<int>,我们想对栈中的每个元素应用一个操作,并将结果存储到一个 std::vector 中。
#include <iostream>
#include <algorithm>
#include <vector>
#include <stack>

// 定义一元操作
int double_number(int num) {
    return num * 2;
}

int main() {
    std::stack<int> int_stack;
    int_stack.push(1);
    int_stack.push(2);
    int_stack.push(3);

    std::vector<int> result(int_stack.size());

    std::stack<int> temp_stack = int_stack;
    for (auto it = result.rbegin(); it != result.rend(); ++it) {
        *it = double_number(temp_stack.top());
        temp_stack.pop();
    }

    for (int num : result) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这个例子中,我们首先创建了一个 std::stack<int>,然后将栈中的元素复制到一个临时栈中,并使用 transform 的思想(这里没有直接使用 transform 函数,因为栈没有随机访问迭代器)对每个元素进行加倍操作,并将结果存储到 std::vector 中。

自定义操作中的捕获变量

在使用 lambda 表达式作为自定义操作时,我们可以捕获外部变量。这在一些复杂的场景中非常有用,例如根据外部变量的值来调整操作。

#include <iostream>
#include <algorithm>
#include <vector>

int main() {
    int factor = 2;
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::vector<int> multiplied_numbers(numbers.size());

    std::transform(numbers.begin(), numbers.end(), multiplied_numbers.begin(), [factor](int num) {
        return num * factor;
    });

    for (int num : multiplied_numbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这个例子中,lambda 表达式捕获了外部变量 factor,并在操作中使用它来将 numbers 向量中的每个元素乘以 factor

性能考虑

在使用 transform 结合自定义操作时,性能是一个需要考虑的因素。特别是当自定义操作比较复杂时,可能会影响算法的执行效率。

  1. 内联操作:如果自定义操作是一个简单的函数,编译器可能会将其内联,从而减少函数调用的开销。例如,简单的算术操作或者逻辑操作。像前面的 squareadd 等函数,编译器很可能会将它们内联。
  2. 避免不必要的拷贝:在自定义操作中,尽量避免不必要的对象拷贝。如果操作涉及到大型对象,通过引用传递参数可以显著提高性能。例如:
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>

class BigObject {
public:
    std::string data;
    BigObject() : data(std::string(10000, 'a')) {}
};

BigObject concatenate(const BigObject& a, const BigObject& b) {
    BigObject result;
    result.data = a.data + b.data;
    return result;
}

int main() {
    std::vector<BigObject> objects1(5);
    std::vector<BigObject> objects2(5);
    std::vector<BigObject> concatenated_objects(5);

    std::transform(objects1.begin(), objects1.end(), objects2.begin(), concatenated_objects.begin(), concatenate);

    return 0;
}

在这个例子中,concatenate 函数通过引用接受 BigObject 参数,避免了不必要的拷贝。如果参数是按值传递,每次调用 concatenate 时都会进行一次拷贝,这会带来较大的性能开销。

  1. 并行化:在 C++17 及更高版本中,可以使用并行算法版本的 transform,通过 std::execution::par 策略来利用多核处理器加速操作。例如:
#include <iostream>
#include <algorithm>
#include <vector>
#include <execution>

int square(int num) {
    return num * num;
}

int main() {
    std::vector<int> numbers(1000000);
    for (size_t i = 0; i < numbers.size(); ++i) {
        numbers[i] = i;
    }
    std::vector<int> squared_numbers(numbers.size());

    std::transform(std::execution::par, numbers.begin(), numbers.end(), squared_numbers.begin(), square);

    return 0;
}

这里使用 std::execution::par 策略来并行执行 transform 操作,对于大数据集可以显著提高执行速度。

常见错误和注意事项

  1. 输出范围大小:确保输出范围足够大以容纳所有结果。如果输出范围太小,可能会导致未定义行为。例如:
#include <iostream>
#include <algorithm>
#include <vector>

int square(int num) {
    return num * num;
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::vector<int> squared_numbers(3); // 输出范围太小

    std::transform(numbers.begin(), numbers.end(), squared_numbers.begin(), square); // 未定义行为

    return 0;
}
  1. 迭代器类型匹配:输入范围和输出范围的迭代器类型必须与 transform 函数的要求相匹配。例如,对于关联容器(如 std::mapstd::set),它们的迭代器不支持随机访问,在使用 transform 时需要注意。
  2. 自定义操作的副作用:尽量避免在自定义操作中引入副作用。transform 算法的设计初衷是对元素进行无副作用的转换操作。如果自定义操作有副作用,可能会导致难以调试的问题,并且不符合算法的设计意图。例如:
#include <iostream>
#include <algorithm>
#include <vector>

int global_variable = 0;

int add_with_side_effect(int num) {
    global_variable += num;
    return num + 1;
}

int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::vector<int> result(numbers.size());

    std::transform(numbers.begin(), numbers.end(), result.begin(), add_with_side_effect);

    std::cout << "global_variable: " << global_variable << std::endl;

    return 0;
}

在这个例子中,add_with_side_effect 函数修改了全局变量 global_variable,这是一个副作用。虽然程序可能会按预期输出,但这种做法破坏了 transform 的无副作用原则,可能会在更复杂的场景中导致问题。

通过深入理解 transform 算法的自定义操作,我们可以在 C++ 编程中更灵活、高效地处理数据。无论是简单的数据转换,还是复杂的业务逻辑处理,transform 都能成为我们的得力工具。同时,注意性能和常见错误,可以使我们编写的代码更加健壮和高效。