Fitur demi fitur sudah kita bahas di section ini, dari C++11 sampai C++26 — tetapi “menulis C++ modern” bukan soal hafal daftar fitur, melainkan soal kebiasaan: memilih {} alih-alih (), menyerahkan kepemilikan memori ke container dan smart pointer, membiarkan compiler menghitung nilai tetap, dan menyusun aliran data lewat views daripada loop berlapis. Chapter ini merangkum kebiasaan itu dalam satu studi kasus yang nyata: program laporan transaksi yang sama ditulis dua kali — sekali dengan gaya pra-C++11, sekali dengan gaya modern — dengan hasil eksekusi yang identik. Membandingkan keduanya berdampingan jauh lebih meyakinkan daripada daftar aturan yang menggantung tanpa bukti.

Studi Kasus: Laporan Transaksi Gaya Lama #

Program berikut adalah kode yang masih banyak kita temukan di proyek lama: container berisi pointer, loop berindeks, dan pengelolaan memori manual. Kode ini sah dan benar — tetapi benar hanya karena disiplin kita, bukan karena bahasanya.

cpp
// sebelum.cpp — gaya pra-C++11: sah, tapi verbose dan rawan
// Kompilasi: g++ -std=c++23 -Wall -Wextra sebelum.cpp -o sebelum

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

struct Transaksi {
    std::string id;
    int nominal;
    bool lunas;

    Transaksi(const std::string& i, int n, bool l) : id(i), nominal(n), lunas(l) {}
};

int main() {
    std::vector<Transaksi*> data;                    // kepemilikan manual
    data.push_back(new Transaksi("TRX-001", 120000, true));
    data.push_back(new Transaksi("TRX-002", 65000, false));
    data.push_back(new Transaksi("TRX-003", 98000, true));
    data.push_back(new Transaksi("TRX-004", 150000, true));

    int nomor = 1;                                   // counter manual
    int totalLunas = 0;
    int totalBelum = 0;
    Transaksi* terbesar = data[0];                   // kandidat awal manual

    for (std::size_t i = 0; i < data.size(); ++i) {
        if (data[i]->lunas) {
            std::cout << nomor << ". " << data[i]->id
                      << " " << data[i]->nominal << std::endl;
            nomor++;
        }
        if (data[i]->lunas) {                        // kondisi sama, diperiksa lagi
            totalLunas += data[i]->nominal;
        } else {
            totalBelum += data[i]->nominal;
        }
        if (data[i]->nominal > terbesar->nominal) {
            terbesar = data[i];
        }
    }

    std::cout << "total LUNAS " << totalLunas << std::endl;
    std::cout << "total BELUM " << totalBelum << std::endl;
    std::cout << "terbesar " << terbesar->id << " "
              << terbesar->nominal << std::endl;

    for (std::size_t i = 0; i < data.size(); ++i) {  // harus diingat manual
        delete data[i];
    }
    return 0;
}

Output yang Dihasilkan:

text
1. TRX-001 120000
2. TRX-003 98000
3. TRX-004 150000
total LUNAS 368000
total BELUM 65000
terbesar TRX-004 150000

Titik-titik lemahnya:

  1. Baris 17-21: std::vector<Transaksi*> berarti setiap elemen dialokasikan dengan new dan harus dilepas dengan delete di baris 49-51. Jika salah satu jalur eksekusi lupa, atau exception terjadi sebelum loop delete, kita mendapat memory leak.
  2. Baris 13: constructor hanya berisi penyalinan parameter ke member — boilerplate yang wajib ditulis hanya agar new Transaksi("...") bisa dipanggil.
  3. Baris 23 dan 32: counter nomor dikelola sendiri, padahal indeks iterasi sebenarnya sudah ada.
  4. Baris 29 dan 34: kondisi data[i]->lunas diperiksa dua kali untuk dua kepentingan berbeda dalam satu loop — tiga perhatian (cetak, jumlahkan, cari terbesar) bercampur dalam satu badan loop.
  5. Baris 31 dan 44-47: std::endl memaksa flush setiap baris (lebih mahal dari '\n'), dan penggabungan << membuang kata-kata untuk formatting.

Refactor: Program yang Sama, Gaya Modern #

Sekarang versi modern — hasil eksekusinya identik byte-per-byte, tetapi tidak ada satu pun new, delete, atau counter manual:

cpp
// sesudah.cpp — gaya modern: deklaratif, aman, hasil yang sama persis
// Kompilasi: g++ -std=c++23 -Wall -Wextra sesudah.cpp -o sesudah

#include <algorithm>
#include <concepts>
#include <print>
#include <ranges>
#include <string>
#include <string_view>
#include <vector>

struct Transaksi {
    std::string id;
    int nominal;
    bool lunas;
};

constexpr std::string_view LABEL_LUNAS = "LUNAS";   // konstanta waktu-kompilasi
constexpr std::string_view LABEL_BELUM = "BELUM";

bool sedangLunas(const Transaksi& t) { return t.lunas; }
bool belumLunas(const Transaksi& t) { return !t.lunas; }

// Concept + ranges: menerima range apa pun yang elemennya bilangan bulat
template <std::ranges::input_range R>
    requires std::integral<std::ranges::range_value_t<R>>
int totalDari(R nilai) {
    return std::ranges::fold_left(nilai, 0, std::plus{});
}

int main() {
    const std::vector<Transaksi> data{                  // value, bukan pointer
        {.id = "TRX-001", .nominal = 120000, .lunas = true},
        {.id = "TRX-002", .nominal = 65000, .lunas = false},
        {.id = "TRX-003", .nominal = 98000, .lunas = true},
        {.id = "TRX-004", .nominal = 150000, .lunas = true},
    };

    // enumerate (C++23) memberi indeks tanpa counter manual
    for (const auto& [nomor, t] :
         data | std::views::filter(sedangLunas) | std::views::enumerate) {
        std::print("{}. {} {}\n", nomor + 1, t.id, t.nominal);
    }

    auto nominalLunas = data | std::views::filter(sedangLunas)
                             | std::views::transform(&Transaksi::nominal);
    auto nominalBelum = data | std::views::filter(belumLunas)
                             | std::views::transform(&Transaksi::nominal);
    std::print("total {} {}\n", LABEL_LUNAS, totalDari(nominalLunas));
    std::print("total {} {}\n", LABEL_BELUM, totalDari(nominalBelum));

    const auto terbesar = std::ranges::max_element(data, {}, &Transaksi::nominal);
    std::print("terbesar {} {}\n", terbesar->id, terbesar->nominal);
    return 0;
}

Output yang Dihasilkan:

text
1. TRX-001 120000
2. TRX-003 98000
3. TRX-004 150000
total LUNAS 368000
total BELUM 65000
terbesar TRX-004 150000

Pemetaan perubahannya:

AspekGaya lama (sebelum.cpp)Gaya modern (sesudah.cpp)
Datavector<Transaksi*> + new/deletevector<Transaksi> berisi nilai; tidak ada alokasi manual
Konstruksiconstructor boilerplate (baris 13)aggregate init dengan designated initializer {.id = ...}
Loopindeks i + counter nomorrange-for + views::enumerate + structured bindings
Jumlahanakumulator manual di dalam loopfold_left di totalDari yang ter-constrain concept
Nilai terbesarloop pembanding manualstd::ranges::max_element dengan projection
Konstantastring literal tersebarconstexpr std::string_view di satu tempat
Keluaranstd::cout << ... << std::endlstd::print terformat

Beberapa keputusan penting di versi modern: data dideklarasikan const karena tidak pernah berubah (const correctness — compiler kini menjaga janji itu untuk kita); structured bindings const auto& [nomor, t] menghindari penulisan tipe iterator yang panjang; dan totalDari memakai concept std::integral atas tipe elemen range, sehingga fungsi kecil itu dipakai dua kali (lunas dan belum) tanpa salin-tempel. Yang tidak terlihat justru paling penting: tanpa new/delete, tidak ada yang bisa bocor — vector melepas seluruh Transaksi otomatis di akhir main, sesuai prinsip RAII.

Checklist Menulis C++ Modern #

Dari seluruh section ini, kebiasaan berikut adalah benang merahnya — gunakan sebagai daftar periksa saat menulis atau meninjau kode:

  • Inisialisasi dengan {} — satu sintaks untuk semua tipe, menolak narrowing lossy (Chapter Uniform Initialization).
  • auto dan structured bindings — tipe ditulis sekali di sebelah kanan; destructuring membuat kode lebih pendek tanpa mengaburkan maksud.
  • Const correctness — const pada variabel, parameter const&, method const; perlakukan const sebagai default, bukan tambahan.
  • constexpr dulu — nilai yang bisa diketahui saat kompilasi sebaiknya dihitung saat kompilasi; sisakan runtime untuk hal yang memang dinamis.
  • Tidak ada new/delete manual — container, std::string, dan smart pointer mengelola memori lewat RAII.
  • Algoritma dan views sebelum loop manual — std::ranges::sort, find, fold_left, pipeline filter | transform menyatakan maksud, bukan mekanisme.
  • Concepts untuk syarat tipe — template yang memasang syarat eksplisit memberi error yang bisa dibaca (Chapter Concepts dan Ranges).
  • std::print/std::format untuk keluaran — format string diperiksa compiler, bukan ditebak saat runtime.

Sekali lagi: gaya modern bukan target untuk semua fitur baru dalam satu file. Loop biasa tetap sah bila paling jelas; yang kita kejar adalah kode yang menyatakan maksud, aman secara default, dan membiarkan pekerjaan rutin diambil alih oleh compiler serta pustaka standar.

Section Modern C++ berhenti di sini — dan kebiasaan di atas menuntut satu pemahaman fondamental yang belum kita dalami: apa sebenarnya yang terjadi ketika kita menyalin objek, dan kapan kita bisa hanya “memindahkan” isinya. Jawabannya ada di Section Move Semantics dan Value Categories, dimulai dari perbedaan lvalue dan rvalue.

Rujukan resmi: cppreference — std::unique_ptr , cppreference — ranges .