Judul chapter ini memuat kata “idiom” dengan sengaja: menghapus elemen dari std::vector berdasarkan nilai ternyata tidak bisa dilakukan satu algoritma saja sebelum C++20. Penyebabnya terletak pada pembagian tugas yang ketat di STL: algoritma bekerja pada rentang iterator dan tidak tahu apa-apa tentang container, sedangkan hanya container yang bisa mengubah ukurannya sendiri. Akibatnya std::remove tidak menghapus apa pun — ia hanya menggeser elemen yang lolos ke depan rentang. Chapter ini membedah perilaku tersebut, merakit idiom klasik erase(remove(...)), lalu berkenalan dengan std::erase_if dari C++20 yang memangkas seluruh kegembengan itu menjadi satu panggilan.

std::remove Tidak Menghapus Apa pun #

Perilaku std::remove(first, last, nilai): elemen yang tidak sama dengan nilai digeser maju berurutan ke depan rentang, lalu fungsi mengembalikan iterator ke akhir rentang logis yang baru. Ukuran container tidak berubah sama sekali; elemen setelah akhir rentang logis tetap ada secara fisik, meskipun isinya tidak lagi dijamin bermakna. Program berikut membuktikannya:

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

void cetak(const std::vector<int>& v) {
    for (std::size_t i = 0; i < v.size(); ++i) {
        if (i > 0) std::cout << ' ';
        std::cout << v[i];
    }
    std::cout << '\n';
}

int main() {
    std::vector<int> data = {4, 8, 6, 8, 2, 8, 9};
    std::cout << "Sebelum      : ";
    cetak(data);
    std::cout << "size awal    : " << data.size() << "\n";

    // std::remove hanya MENGGESER elemen: yang lolos maju ke depan
    auto logis = std::remove(data.begin(), data.end(), 8);

    // Rentang logis [begin, logis) berisi elemen yang tersisa
    std::cout << "Setelah remove: ";
    for (auto it = data.begin(); it != logis; ++it) {
        if (it != data.begin()) std::cout << ' ';
        std::cout << *it;
    }
    std::cout << "\n";
    std::cout << "size tetap   : " << data.size() << "\n";

    // erase: benar-benar menghapus sisa [logis, end)
    data.erase(logis, data.end());
    std::cout << "Setelah erase : ";
    cetak(data);
    std::cout << "size sekarang : " << data.size() << "\n";
    return 0;
}

Output yang Dihasilkan:

text
Sebelum      : 4 8 6 8 2 8 9
size awal    : 7
Setelah remove: 4 6 2 9
size tetap   : 7
Setelah erase : 4 6 2 9
size sekarang : 4

Bedah baris demi baris:

  1. Baris 15-16: data awal {4, 8, 6, 8, 2, 8, 9} dengan size tujuh.
  2. Baris 19 (std::remove(data.begin(), data.end(), 8)): setiap elemen yang bukan 8 digeser maju. Hasilnya rentang logis 4 6 2 9, dan logis adalah iterator ke posisi setelah elemen terakhir yang tersisa.
  3. Baris 22-27: kita mencetak hanya [data.begin(), logis) — rentang yang dijamin standar. Yang berada setelah logis sengaja tidak dicetak karena standar tidak menjamin isinya; pada GCC 14 nilai lama memang masih terlihat, tetapi mengandalkannya adalah kesalahan.
  4. Baris 29: size masih tujuh. Inilah bukti bahwa remove tidak menyentuh ukuran container.
  5. Baris 32-33 (data.erase(logis, data.end())): member erase milik vector-lah yang benar-benar menghapus, dan size akhirnya turun menjadi empat.

Pemisahan tugas ini konsisten dengan desain STL: remove hanya butuh iterator dan bisa bekerja di array C-style yang tidak bisa mengubah ukuran, sedangkan mengubah ukuran adalah wewenang container. Harga bayarannya adalah dua langkah untuk satu maksud.

Idiom erase(remove(…)) #

Kedua langkah itu dirangkai menjadi satu pernyataan yang dikenal luas sebagai remove-erase idiom:

cpp
v.erase(std::remove_if(v.begin(), v.end(), predikat), v.end());

Bacaannya dari dalam ke luar: remove_if menggeser elemen yang lolos predikat ke depan dan mengembalikan akhir rentang logis; erase kemudian menghapus dari posisi itu sampai v.end(). Hasil akhirnya: elemen yang lolos predikat hilang dari container, size menyusut, dan urutan elemen tersisa dipertahankan. Program berikut memakai idiom itu lalu lanjut ke penggantinya:

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

void cetak(const std::vector<int>& v) {
    for (std::size_t i = 0; i < v.size(); ++i) {
        if (i > 0) std::cout << ' ';
        std::cout << v[i];
    }
    std::cout << '\n';
}

int main() {
    std::vector<int> data = {5, 12, 8, 15, 3, 20, 7};

    // Idiom klasik: erase(remove_if(...)) dalam satu pernyataan
    data.erase(std::remove_if(data.begin(), data.end(),
                              [](int x) { return x % 2 != 0; }),
               data.end());
    std::cout << "erase(remove_if) : ";
    cetak(data);

    // Modernisasi C++20: std::erase_if, satu panggilan tanpa idiom
    std::erase_if(data, [](int x) { return x > 10; });
    std::cout << "erase_if         : ";
    cetak(data);
    return 0;
}

Output yang Dihasilkan:

text
erase(remove_if) : 12 8 20
erase_if         : 8

Baris pertama output menunjukkan vector setelah semua elemen ganjil dihapus lewat idiom klasik ({12, 8, 20}), dan baris kedua setelah std::erase_if(data, [](int x) { return x > 10; }) menghapus semua yang di atas sepuluh, menyisakan {8}. Idiom klasik dan erase_if menghasilkan efek yang identik; yang berbeda hanya jumlah kode dan peluang salah tulis.

erase_if: Modernisasi C++20 #

Karena pola idiom itu mudah salah tulis dan tidak sedikit pula yang tertukar antara remove, remove_if, dan argumen erase, standar C++20 menambahkan std::erase_if(c, predikat) serta std::erase(c, nilai) sebagai overload khusus container: vector, string, list, map, set, dan lainnya. Fungsi ini milik header container yang bersangkutan (untuk vector, <vector>), menerima container langsung, mengubah ukurannya, dan mengembalikan jumlah elemen yang terhapus.

Sampai di sini kita sudah bisa menambah, menyalin, mengubah, dan mengurangi elemen. Chapter berikutnya melangkah ke algoritma pengelompokan: std::partition yang membagi rentang menjadi dua kelompok menurut predikat tanpa perlu menyortir penuh.

Rujukan resmi: std::remove dan remove_if serta std::erase_if untuk vector .