Skip to content

Modifying Algorithms — copy, move, transform, replace, Erase-Remove Idiom

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Modifying Algorithms. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ modifying algorithms mutate ranges through copy, move, transform, replace, fill, generate, and remove operations, with the erase-remove idiom permanently deleting elements.

What You'll Learn

You will copy and move data between containers with std::copy and std::move, apply functions element-wise with std::transform, replace or conditionally replace elements with std::replace and std::replace_if, fill and generate sequences with std::fill, std::generate, and std::iota, and permanently delete elements using the erase-remove idiom.

Why It Matters

Modifying algorithms replace manual loops for common data manipulation tasks. They Express intent clearly, avoid off-by-one errors, and are often optimized using SIMD instructions or memcpy. The erase-remove idiom in particular eliminates an entire class of bugs related to iterating while modifying.

Learning Path

graph LR
    A["37: Sorting & Searching"] --> B["38: Modifying Algorithms"]
    B --> C["39: Numeric Algorithms"]
    C --> D["40: Ranges Library"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

std::copy and std::copy_if

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

int main() {
    std::vector<int> source = {1, 2, 3, 4, 5};
    std::vector<int> dest;
    
    // copy from source to back of dest
    std::copy(source.begin(), source.end(), std::back_inserter(dest));
    
    // copy_if (conditional)
    std::vector<int> evens;
    std::copy_if(source.begin(), source.end(),
                 std::back_inserter(evens),
                 [](int x) { return x % 2 == 0; });
    
    // copy_n (first n elements)
    std::vector<int> first3;
    std::copy_n(source.begin(), 3, std::back_inserter(first3));
    
    // Raw array copy
    int arr[5];
    std::copy(source.begin(), source.end(), arr);
    
    for (int x : dest) std::cout << x << " ";
    std::cout << "\n";
}

std::move (Algorithm)

#include <iostream>
#include <vector>
#include <algorithm>
#include <iterator>
#include <memory>

int main() {
    std::vector<std::unique_ptr<int>> source;
    source.push_back(std::make_unique<int>(10));
    source.push_back(std::make_unique<int>(20));
    source.push_back(std::make_unique<int>(30));
    
    std::vector<std::unique_ptr<int>> dest;
    
    // Move elements from source to dest
    std::move(source.begin(), source.end(), std::back_inserter(dest));
    
    // source elements are now in moved-from state (nullptr)
    std::cout << "source[0] is null: " << (source[0] == nullptr) << "\n";
    std::cout << "dest[0]: " << *dest[0] << "\n";
}

std::transform

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

int main() {
    std::vector<int> nums = {1, 2, 3, 4, 5};
    
    // Unary transform: apply function to each element
    std::vector<int> squares;
    std::transform(nums.begin(), nums.end(),
                   std::back_inserter(squares),
                   [](int x) { return x * x; });
    
    // Binary transform: combine two ranges
    std::vector<int> a = {1, 2, 3};
    std::vector<int> b = {4, 5, 6};
    std::vector<int> sum;
    std::transform(a.begin(), a.end(), b.begin(),
                   std::back_inserter(sum),
                   [](int x, int y) { return x + y; });
    
    // Transform in place
    std::transform(nums.begin(), nums.end(), nums.begin(),
                   [](int x) { return x * 10; });
    
    // String transformation
    std::string s = "hello";
    std::transform(s.begin(), s.end(), s.begin(),
                   [](unsigned char c) { return std::toupper(c); });
    std::cout << s << "\n";  // HELLO
}

std::replace and std::replace_if

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

int main() {
    std::vector<int> v = {1, 2, 3, 2, 4, 2, 5};
    
    // Replace all 2s with 99
    std::replace(v.begin(), v.end(), 2, 99);
    
    for (int x : v) std::cout << x << " ";
    std::cout << "\n";  // 1 99 3 99 4 99 5
    
    // Conditional replace
    std::replace_if(v.begin(), v.end(),
                    [](int x) { return x % 2 == 0; }, 0);
    
    // Copy versions don't modify original
    std::vector<int> original = {1, 2, 3, 2, 4};
    std::vector<int> result;
    std::replace_copy(original.begin(), original.end(),
                      std::back_inserter(result), 2, 99);
    // original unchanged, result has replacements
}

std::fill, std::generate, std::iota

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

int main() {
    std::vector<int> v(10);
    
    // fill with constant
    std::fill(v.begin(), v.begin() + 3, -1);
    
    // fill_n (fill first n elements)
    std::fill_n(v.begin(), 3, -2);
    
    // generate with function
    int n = 0;
    std::generate(v.begin(), v.end(), [&n]() { return n++; });
    // v now contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
    
    // iota: fill with incrementing values
    std::iota(v.begin(), v.end(), 100);
    // v now contains 100, 101, 102, ...
    
    for (int x : v) std::cout << x << " ";
    std::cout << "\n";
}

The Erase-Remove Idiom

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

int main() {
    std::vector<int> v = {1, 2, 3, 2, 4, 2, 5, 2, 6};
    
    // Step 1: remove shifts non-removed elements to the front
    // Step 2: erase the "tail" of removed elements
    
    // Remove all 2s
    auto newEnd = std::remove(v.begin(), v.end(), 2);
    v.erase(newEnd, v.end());
    
    for (int x : v) std::cout << x << " ";
    std::cout << "\n";  // 1 3 4 5 6
    
    // Remove_if + erase (one line)
    v.erase(std::remove_if(v.begin(), v.end(),
                [](int x) { return x % 2 == 1; }),
            v.end());
    
    for (int x : v) std::cout << x << " ";
    std::cout << "\n";  // 4 6
    
    // std::remove does not change size
    std::vector<int> demo = {1, 2, 3};
    std::cout << "Before remove: " << demo.size() << "\n";  // 3
    std::remove(demo.begin(), demo.end(), 1);
    std::cout << "After remove: " << demo.size() << "\n";   // 3 (still!)
    // Elements are: [2, 3, ?] — size unchanged
}

Removing Duplicates with unique

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

int main() {
    std::vector<int> v = {1, 2, 2, 3, 3, 3, 4, 5, 5, 6};
    
    // unique only removes CONSECUTIVE duplicates
    // Must sort first if you want to remove all duplicates
    auto newEnd = std::unique(v.begin(), v.end());
    v.erase(newEnd, v.end());
    
    for (int x : v) std::cout << x << " ";
    std::cout << "\n";  // 1 2 3 4 5 6
    
    // Using unique with custom predicate
    std::vector<int> v2 = {1, 2, 4, 3, 6, 5, 8, 7};
    std::sort(v2.begin(), v2.end());
    newEnd = std::unique(v2.begin(), v2.end(),
                         [](int a, int b) { return a % 2 == b % 2; });
    v2.erase(newEnd, v2.end());
    // Keeps one even and one odd: e.g., 2, 3 (implementation-dependent)
}

std::reverse, std::rotate, std::shuffle

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

int main() {
    std::vector<int> v = {1, 2, 3, 4, 5, 6, 7};
    
    // reverse
    std::reverse(v.begin(), v.end());
    // 7 6 5 4 3 2 1
    
    // rotate: shift elements around a middle point
    auto mid = v.begin() + 3;
    std::rotate(v.begin(), mid, v.end());
    // v now: 4 3 2 1 7 6 5
    
    // shuffle (C++11)
    std::random_device rd;
    std::mt19937 gen(rd());
    std::shuffle(v.begin(), v.end(), gen);
    
    for (int x : v) std::cout << x << " ";
    std::cout << "\n";
}

Common Mistakes

Mistake 1: Forgetting Output Iterator in Copy

std::vector<int> dest;
std::copy(source.begin(), source.end(), dest.begin());  // UB: dest is empty

Use std::back_inserter(dest) to push elements.

Mistake 2: Not Enough Space for Output

int arr[5];
std::copy_n(source.begin(), 10, arr);  // buffer overflow!

Mistake 3: Using remove Without erase

std::remove(v.begin(), v.end(), 3);  // size unchanged, v has garbage at end

Always use the erase-remove idiom.

Mistake 4: Using unique on Unsorted Range

unique only removes consecutive duplicates. To remove all duplicates, sort first or use std::adjacent_find in a loop.

Mistake 5: Forgetting That transform Can Be In-Place

Passing the same range as source and destination is valid for transform (in-place modification).

Mistake 6: Using reverse on Non-Bidirectional Iterators

std::forward_list<int> fl = {1, 2, 3};
// std::reverse(fl.begin(), fl.end());  // Error

Use fl.reverse() member function.

Practice Questions

  1. What is the difference between std::copy and std::copy_if?
  2. Why does std::remove not delete elements?
  3. What does std::unique do? When does it require a sorted range?
  4. Write a one-liner that replaces all odd numbers with 0 in a vector.
  5. How do you fill a vector with the numbers 1 through 100?

Challenge

Implement a function normalize that takes a vector of strings, trims leading/trailing whitespace, converts to lowercase, removes empty strings, and removes duplicates. Use STL algorithms throughout (no manual loops).

FAQ

Is `std::copy` faster than `memcpy` for trivially copyable types?

Implementations often detect trivially copyable types and optimize to memcpy or memmove. So std::copy is usually just as fast.

What is the difference between `std::move` (algorithm) and `std::move` (cast)?

std::move (cast) converts an lvalue to an rvalue reference. std::move (algorithm) moves elements from one range to another. They are different things with the same name.

Does `std::transform` work with different input and output types?

Yes. The input and output iterator types can differ. For example, transform int vector to bool vector.

What is the difference between `std::fill` and `std::generate`?

fill sets all elements to the same constant value. generate calls a function for each element, allowing different values.

Why is there a `std::remove` but no `std::erase`?

remove can work with any forward iterator and just shifts elements. erase needs container-specific knowledge (size, capacity). The erase-remove idiom combines them.

What happens to the elements after the new end in erase-remove?

They are in a valid but unspecified state. The container still owns them (they are not destroyed) until erase is called.

Mini Project

Build an image processing pipeline using modifying algorithms:

#include <iostream>
#include <vector>
#include <algorithm>
#include <random>
#include <numeric>

struct Image {
    std::vector<int> pixels;
    int width, height;
    
    Image(int w, int h) : width(w), height(h), pixels(w * h) {}
    
    void fill(int value) {
        std::fill(pixels.begin(), pixels.end(), value);
    }
    
    void applyBrightness(int delta) {
        std::transform(pixels.begin(), pixels.end(), pixels.begin(),
                       [delta](int p) {
                           return std::clamp(p + delta, 0, 255);
                       });
    }
    
    void applyThreshold(int threshold) {
        std::transform(pixels.begin(), pixels.end(), pixels.begin(),
                       [threshold](int p) {
                           return p >= threshold ? 255 : 0;
                       });
    }
    
    void invert() {
        std::transform(pixels.begin(), pixels.end(), pixels.begin(),
                       [](int p) { return 255 - p; });
    }
    
    void addNoise(int amount) {
        std::default_random_engine gen;
        std::uniform_int_distribution dist(-amount, amount);
        std::transform(pixels.begin(), pixels.end(), pixels.begin(),
                       [&](int p) {
                           return std::clamp(p + dist(gen), 0, 255);
                       });
    }
    
    void print() const {
        for (int y = 0; y < height; ++y) {
            for (int x = 0; x < width; ++x) {
                char c = pixels[y * width + x] > 128 ? '#' : ' ';
                std::cout << c;
            }
            std::cout << "\n";
        }
    }
};

int main() {
    Image img(20, 10);
    img.fill(100);
    img.addNoise(50);
    img.invert();
    img.applyThreshold(128);
    img.print();
}

What's Next

Modifying algorithms transform data. The next lesson covers numeric algorithms: accumulate, inner_product, partial_sum, iota, adjacent_difference, and parallel execution policies.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro