Алгоритм std::transform на C++  —  универсальный инструмент, которым заданная функция применяется к диапазону элементов, результат при этом сохраняется в другом диапазоне.

Благодаря этой ключевой части стандартной библиотеки шаблонов значительно упрощается код при работе с коллекциями данных.

Что такое std::transform?

В std::transform принимается диапазон входных элементов, к каждому элементу применяется функция и результаты записываются в выходной диапазон. std::transform определен в заголовке <algorithm> и представлен двумя основными разновидностями:

  1. Унарная операция с преобразованием одного входного диапазона.
  2. Бинарная операция с объединением элементов двух входных диапазонов.

Изучим работу std::transform на практических примерах.

Базовое применение: преобразование вектора

При помощи std::transform удваивается каждый элемент вектора целых чисел:

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

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(),
[](int n) { return n * 2; });

for (int n : result) {
std::cout << n << " ";
}
// Вывод: 2 4 6 8 10

return 0;
}

В этом примере в качестве операции преобразования используется лямбда-функция [](int n) { return n * 2; }. Функцией transform она применяется к каждому элементу numbers, результаты сохраняются в result.

Преобразование на месте

Также при помощи std::transform на месте изменяется контейнер:

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

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

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

for (int n : numbers) {
std::cout << n << " ";
}
// Вывод: 1 4 9 16 25

return 0;
}

Здесь каждое число в векторе возводится в квадрат, при этом исходные значения перезаписываются.

Работа с пользовательскими типами

std::transform не ограничивается встроенными типами. Так он применяется пользовательским классом Person:

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

class Person {
public:
Person(std::string name, int age) : name_(name), age_(age) {}
std::string name() const { return name_; }
int age() const { return age_; }
private:
std::string name_;
int age_;
};

int main() {
std::vector<Person> people = {
{"Alice", 25},
{"Bob", 30},
{"Charlie", 35}
};

std::vector<std::string> names(people.size());

std::transform(people.begin(), people.end(), names.begin(),
[](const Person& p) { return p.name(); });

for (const auto& name : names) {
std::cout << name << " ";
}
// Вывод: Alice Bob Charlie

return 0;
}

В этом примере из вектора объектов Person имена извлекаются в новый вектор строк.

Бинарные операции: объединение двух диапазонов

Бинарной версией std::transform комбинируются элементы двух диапазонов:

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

int main() {
std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = {10, 20, 30, 40, 50};
std::vector<int> result(v1.size());

std::transform(v1.begin(), v1.end(), v2.begin(), result.begin(),
[](int a, int b) { return a + b; });

for (int n : result) {
std::cout << n << " ";
}
// Вывод: 11 22 33 44 55

return 0;
}

В этом коде добавляются соответствующие элементы из v1 и v2, суммы сохраняются в result.

Реальный сценарий: конвейер обработки данных

Рассмотрим пример посложнее, где std::transform является частью конвейера обработки данных:

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
#include <cctype>

// Этап 1: преобразуются в верхний регистр
std::string to_upper(const std::string& s) {
std::string result = s;
std::transform(s.begin(), s.end(), result.begin(),
[](unsigned char c) { return std::toupper(c); });
return result;
}

// Этап 2: подсчитываются гласные
int count_vowels(const std::string& s) {
return std::count_if(s.begin(), s.end(),
[](char c) {
c = std::tolower(c);
return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
});
}

int main() {
std::vector<std::string> words = {"hello", "world", "C++", "transform"};
std::vector<std::string> uppercase_words(words.size());
std::vector<int> vowel_counts(words.size());

// Этап 1: все слова преобразуются в верхний регистр
std::transform(words.begin(), words.end(), uppercase_words.begin(), to_upper);

// Этап 2: в каждом слове в верхнем регистре подсчитываются гласные
std::transform(uppercase_words.begin(), uppercase_words.end(), vowel_counts.begin(), count_vowels);

// Выводятся результаты
for (size_t i = 0; i < words.size(); ++i) {
std::cout << words[i] << " -> " << uppercase_words[i]
<< " (vowels: " << vowel_counts[i] << ")\n";
}

return 0;
}

Здесь показан двухэтапный процесс:

  1. Пользовательской функцией to_upper с помощью std::transform все слова преобразуются в верхний регистр.
  2. При помощи count_vowels и другого std::transform подсчитываются гласные каждого слова в верхнем регистре.

Выводится вот что:

hello -> HELLO (vowels: 2)
world -> WORLD (vowels: 1)
C++ -> C++ (vowels: 0)
transform -> TRANSFORM (vowels: 3)

Так std::transform используется в сложных сценариях, где объединяются многочисленные операции.

Производительность

Хотя std::transform в целом эффективен, имейте в виду:

  1. Встраивание: при простых операциях компилятором часто встраиваются вызовы функций, так что std::transform становится столь же быстрым, как написанный вручную цикл.
  2. Распараллеливание: в C++17 появились политики параллельного выполнения, благодаря которым std::transform на больших наборах данных потенциально ускоряется:
#include <execution>
// ...
std::transform(std::execution::par, v1.begin(), v1.end(), v2.begin(),
[](int n) { return expensive_operation(n); });

Так компилятору указывается на распараллеливание операции, если это возможно.

3. Выделение памяти: при преобразовании в новый контейнер, чтобы избежать перераспределений, не забывайте резервировать место:

std::vector<int> result;
result.reserve(numbers.size());
std::transform(numbers.begin(), numbers.end(), std::back_inserter(result),
[](int n) { return n * 2; });

Типичные ошибки и как их избежать

  1. Недостаточно места для выходного итератора. Если не используется std::back_inserter, целевой контейнер должен быть не меньше входного диапазона.
  2. Для повышения производительности сложных операций используйте не лямбда-выражения, а объекты-функции:
struct Multiplier {
int factor;
Multiplier(int f) : factor(f) {}
int operator()(int n) const { return n * factor; }
};

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

3. Будьте осторожны при преобразовании контейнера на месте и изменении его размера, это чревато инвалидацией итераторов:

// Некорректно, поскольку чревато неопределенным поведением
std::transform(vec.begin(), vec.end(), std::back_inserter(vec),
[](int n) { return n * 2; });

Преобразуйте в отдельный контейнер, а затем меняйте:

std::vector<int> result;
result.reserve(vec.size());
std::transform(vec.begin(), vec.end(), std::back_inserter(result),
[](int n) { return n * 2; });
vec.swap(result);

Заключение

std::transform  —  это гибкий, эффективный инструмент в арсенале программиста на C++. С std::transform упрощаются многие типичные операции контейнеров, а код становится более чистым и сопровождаемым.

Изучив нюансы std::transform, вы сможете эффективно использовать его в проектах на C++  —  от простых преобразований до сложных конвейеров данных.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи ryan: C++ Transform: Practical Guide

Предыдущая статья7 ключевых вопросов на собеседовании по JavaScript
Следующая статьяОптимизация кэширования в TrendNow: объединение OkHttp Cache и базы данных Room. Часть 7