Алгоритм std::transform на C++ — универсальный инструмент, которым заданная функция применяется к диапазону элементов, результат при этом сохраняется в другом диапазоне.
Благодаря этой ключевой части стандартной библиотеки шаблонов значительно упрощается код при работе с коллекциями данных.
Что такое std::transform?
В std::transform принимается диапазон входных элементов, к каждому элементу применяется функция и результаты записываются в выходной диапазон. std::transform определен в заголовке <algorithm> и представлен двумя основными разновидностями:
- Унарная операция с преобразованием одного входного диапазона.
- Бинарная операция с объединением элементов двух входных диапазонов.
Изучим работу 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;
}
Здесь показан двухэтапный процесс:
- Пользовательской функцией
to_upperс помощьюstd::transformвсе слова преобразуются в верхний регистр. - При помощи
count_vowelsи другогоstd::transformподсчитываются гласные каждого слова в верхнем регистре.
Выводится вот что:
hello -> HELLO (vowels: 2)
world -> WORLD (vowels: 1)
C++ -> C++ (vowels: 0)
transform -> TRANSFORM (vowels: 3)
Так std::transform используется в сложных сценариях, где объединяются многочисленные операции.
Производительность
Хотя std::transform в целом эффективен, имейте в виду:
- Встраивание: при простых операциях компилятором часто встраиваются вызовы функций, так что
std::transformстановится столь же быстрым, как написанный вручную цикл. - Распараллеливание: в 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; });
Типичные ошибки и как их избежать
- Недостаточно места для выходного итератора. Если не используется std::back_inserter, целевой контейнер должен быть не меньше входного диапазона.
- Для повышения производительности сложных операций используйте не лямбда-выражения, а объекты-функции:
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++ — от простых преобразований до сложных конвейеров данных.
Читайте также:
- C++: подробное руководство по размерам векторов
- C++: полное руководство по push_back
- C++: полное руководство по преобразованию строки в число двойной точности
Читайте нас в Telegram, VK и Дзен
Перевод статьи ryan: C++ Transform: Practical Guide





