Transforming and Mutating Algorithms
Deep reference for std::transform, for_each, copy, fill, generate, remove, rotate, and C++20 ranges equivalents with pitfalls and best practices.
Transforming Algorithmssince C++98Mutating sequence algorithms that apply a function, copy, fill, reorder, or remove elements in-place or into an output range, without changing the container's logical structure beyond the requested transformation.
Overview
The transforming and mutating algorithms in <algorithm> cover everything from pure element-mapping (std::transform) through reordering (std::rotate, std::shuffle) to selective removal (std::remove_if). The critical design distinction: most algorithms do not resize containers β they write into pre-existing ranges or require an insert iterator. Getting this wrong causes silent UB.
std::iota from <numeric> (C++11) fills a range with successive incremented values and belongs alongside std::fill and std::generate in any daily toolkit.
std::transform
std::transform (C++98) applies a callable to each element and writes results into an output range. The output range must have at least last - first elements pre-allocated, or you must use an inserter.
#include <algorithm>
#include <vector>
#include <functional>
#include <string>
#include <cctype>
// Unary: square each element
std::vector<int> v{1, 2, 3, 4, 5};
std::vector<int> out(v.size());
std::transform(v.begin(), v.end(), out.begin(),
[](int x) { return x * x; });
// out: {1, 4, 9, 16, 25}
// Binary overload (C++98): element-wise combine β second range has no end iterator,
// it must contain at least as many elements as the first or the behavior is undefined
std::vector<int> a{1, 2, 3}, b{10, 20, 30}, sum(3);
std::transform(a.begin(), a.end(), b.begin(), sum.begin(),
std::plus<int>{});
// sum: {11, 22, 33}
// In-place: safe when d_first == first (ranges do not overlap adversely)
std::transform(v.begin(), v.end(), v.begin(),
[](int x) { return x * 2; });
// v: {2, 4, 6, 8, 10}
// String to uppercase β cast to unsigned char before toupper; passing signed char is UB
std::string s = "hello world";
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return std::toupper(c); });
// s: "HELLO WORLD"std::for_each
std::for_each (C++98) applies a callable to each element and returns the callable. This matters for stateful functors β the returned object carries accumulated state.
#include <algorithm>
#include <execution> // C++17
#include <vector>
std::vector<int> v{1, 2, 3, 4, 5};
// Capture the returned functor to access its accumulated state
struct Accumulator {
long long sum = 0;
void operator()(int x) { sum += x; }
};
auto acc = std::for_each(v.begin(), v.end(), Accumulator{});
// acc.sum == 15
// Parallel for_each (C++17): callable must be invocable concurrently without data races
std::for_each(std::execution::par_unseq, v.begin(), v.end(),
[](int& x) { x *= 2; }); // safe: each element is independent
// for_each_n (C++17): apply to exactly n elements starting at first
std::for_each_n(v.begin(), 3, [](int& x) { x = 0; });
// v[0..2] zeroed, v[3..4] unchangedFill, Generate, and Iota
#include <algorithm>
#include <numeric> // std::iota β C++11
#include <vector>
#include <random>
std::vector<int> v(5);
std::fill(v.begin(), v.end(), 42); // {42, 42, 42, 42, 42}
std::fill_n(v.begin(), 3, 0); // {0, 0, 0, 42, 42}
// generate: callable provides successive values; may carry state
int n = 0;
std::generate(v.begin(), v.end(), [&n] { return n++; });
// v: {0, 1, 2, 3, 4}
// generate_n with back_inserter appends to the container
std::vector<int> r;
std::mt19937 rng{42};
std::generate_n(std::back_inserter(r), 5,
[&rng] { return static_cast<int>(rng() % 100); });
// iota (C++11): fills with value, value+1, value+2, ... β cleaner than generate + counter
std::iota(v.begin(), v.end(), 1); // {1, 2, 3, 4, 5}Copy Algorithms
#include <algorithm>
#include <vector>
#include <string>
std::vector<int> src{1, 2, 3, 4, 5};
std::vector<int> dst(5);
std::copy(src.begin(), src.end(), dst.begin());
// copy_if: filter into output β output must be pre-sized or use back_inserter
std::vector<int> evens;
std::copy_if(src.begin(), src.end(), std::back_inserter(evens),
[](int x) { return x % 2 == 0; }); // {2, 4}
// copy_n: exactly n elements
std::copy_n(src.begin(), 3, dst.begin()); // dst[0..2] = {1, 2, 3}
// Overlapping ranges: use copy_backward when dst overlaps src and dst > src
// (shifting elements right within the same container)
std::copy_backward(src.begin(), src.end(), dst.end());
// reverse_copy: writes src reversed into dst, src is unmodified
std::vector<int> rev(src.size());
std::reverse_copy(src.begin(), src.end(), rev.begin()); // {5, 4, 3, 2, 1}
// move (C++11): like copy but leaves source in valid-but-unspecified state
std::vector<std::string> strs{"a", "b", "c"};
std::vector<std::string> moved(3);
std::move(strs.begin(), strs.end(), moved.begin());Replace
std::vector<int> v{1, 2, 3, 2, 1};
std::replace(v.begin(), v.end(), 2, 99);
// v: {1, 99, 3, 99, 1}
std::replace_if(v.begin(), v.end(),
[](int x) { return x > 5; }, 0);
// replace_copy: non-destructive β writes replaced result into separate output
std::vector<int> out(v.size());
std::replace_copy(v.begin(), v.end(), out.begin(), 1, 100);Remove and the Erase-Remove Idiom
std::remove and std::remove_if shift surviving elements toward the front and return an iterator to the new logical end. The container is not resized. Elements past the returned iterator are valid but unspecified β failing to erase them is a common bug.
std::vector<int> v{1, 2, 3, 2, 4, 2, 5};
// Pre-C++20 erase-remove idiom
auto new_end = std::remove(v.begin(), v.end(), 2);
v.erase(new_end, v.end());
// v: {1, 3, 4, 5}
// C++20: std::erase / std::erase_if collapse the idiom into one call
// Available for all standard sequence and associative containers
std::erase(v, 3);
std::erase_if(v, [](int x) { return x % 2 == 0; });Reordering: Reverse, Rotate, Shuffle
#include <algorithm>
#include <random>
#include <vector>
std::vector<int> v{1, 2, 3, 4, 5};
std::reverse(v.begin(), v.end()); // {5, 4, 3, 2, 1}
// rotate: [middle, end) becomes the new front
// Returns iterator to the element that was originally at v.begin() (C++11)
auto it = std::rotate(v.begin(), v.begin() + 2, v.end());
// v: {3, 2, 1, 5, 4}; *it == 5 (was v[0])
// shuffle (C++11): std::random_shuffle removed in C++17
std::mt19937 rng{std::random_device{}()};
std::shuffle(v.begin(), v.end(), rng);
// Permutations: visit all n! orderings in lexicographic order
std::vector<int> p{1, 2, 3};
do {
// process each permutation
} while (std::next_permutation(p.begin(), p.end()));Unique
std::unique removes consecutive duplicates only. Sort first for global deduplication.
std::vector<int> v{3, 1, 1, 2, 3, 3, 4, 1};
std::sort(v.begin(), v.end());
auto new_end = std::unique(v.begin(), v.end());
v.erase(new_end, v.end());
// v: {1, 2, 3, 4}
// C++20: ranges::unique returns a subrange [new_end, old_end)
#include <ranges>
std::ranges::sort(v);
auto [rm, end] = std::ranges::unique(v);
v.erase(rm, end);C++20 Ranges
Ranges versions accept range arguments directly, support projections, and compose with lazy views. They live in std::ranges:: and return rich result types instead of bare iterators.
#include <algorithm>
#include <ranges>
#include <vector>
#include <string>
struct Employee { std::string name; int salary; };
std::vector<Employee> staff{{"Alice", 90000}, {"Bob", 75000}, {"Carol", 95000}};
// transform with projection: extract salary into separate vector
std::vector<int> salaries;
std::ranges::transform(staff, std::back_inserter(salaries),
&Employee::salary);
// copy_if with projection: predicate receives the projected value
std::vector<Employee> senior;
std::ranges::copy_if(staff, std::back_inserter(senior),
[](int s) { return s >= 90000; },
&Employee::salary);
// Lazy pipeline: no intermediate containers allocated
auto top_names = staff
| std::views::filter([](const Employee& e) { return e.salary > 80000; })
| std::views::transform(&Employee::name);
for (const auto& name : top_names)
std::println("{}", name); // std::println: C++23Best Practices
- Prefer
std::iotaoverstd::generatewith an external counter β intent is explicit and the implementation is typically optimized. - Pre-allocate output with
resizewhen size is known; usestd::back_inserterwhen it isn't. Never write into an empty-default-constructed container. - Capture the return value of
std::for_eachwhen using stateful functors β the standard guarantees the returned object reflects all applications. - Prefer
std::erase/std::erase_if(C++20) over the erase-remove idiom. They're single-call, container-aware, and eliminate the erroneous-truncation footgun. - Restrict
std::execution::par_unseqto callable objects that are genuinely thread-safe and operate on disjoint data. Shared mutable state without synchronization is UB.
Common Pitfalls
Unsized output range. std::transform, std::copy, and related algorithms write into an existing range β they never grow it. Writing past the end is UB.
std::vector<int> src{1, 2, 3};
std::vector<int> dst; // empty!
// UB: std::copy(src.begin(), src.end(), dst.begin());
std::copy(src.begin(), src.end(), std::back_inserter(dst)); // correctForgetting to erase after std::remove. std::remove reorganizes elements but leaves the container at its original size with unspecified values past the logical end. Always pair with erase, or use std::erase (C++20).
std::toupper on signed char. Passing a char with a negative value to std::toupper is undefined behavior. Cast to unsigned char before the call.
Binary std::transform overrun. The second input range has no end iterator. If it's shorter than the first range, the algorithm reads past its end β UB with no compile-time diagnostic.
Overlapping copies shifted right. std::copy requires d_first to not lie in (first, last). When shifting elements right within the same container (e.g., making room at the front), use std::copy_backward instead.
std::random_shuffle was removed in C++17. Code using it will not compile in C++17 or later. Replace with std::shuffle and an explicit std::mt19937.
See Also
- Sorting Algorithms β pair with
std::uniquefor deduplication, orstd::partial_sortbefore slicing - Searching Algorithms β locate elements before transforming them
- Ranges and Views β lazy composition with
std::views::transformandstd::views::filter <numeric>Algorithms βstd::iota,std::accumulate,std::transform_reduce(C++17)- Execution Policies β parallel and vectorized variants (C++17)