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.
// 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:
1. TRX-001 120000
2. TRX-003 98000
3. TRX-004 150000
total LUNAS 368000
total BELUM 65000
terbesar TRX-004 150000Titik-titik lemahnya:
- Baris 17-21:
std::vector<Transaksi*>berarti setiap elemen dialokasikan dengannewdan harus dilepas dengandeletedi baris 49-51. Jika salah satu jalur eksekusi lupa, atau exception terjadi sebelum loopdelete, kita mendapat memory leak. - Baris 13: constructor hanya berisi penyalinan parameter ke member — boilerplate yang wajib ditulis hanya agar
new Transaksi("...")bisa dipanggil. - Baris 23 dan 32: counter
nomordikelola sendiri, padahal indeks iterasi sebenarnya sudah ada. - Baris 29 dan 34: kondisi
data[i]->lunasdiperiksa dua kali untuk dua kepentingan berbeda dalam satu loop — tiga perhatian (cetak, jumlahkan, cari terbesar) bercampur dalam satu badan loop. - Baris 31 dan 44-47:
std::endlmemaksa 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:
// 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:
1. TRX-001 120000
2. TRX-003 98000
3. TRX-004 150000
total LUNAS 368000
total BELUM 65000
terbesar TRX-004 150000Pemetaan perubahannya:
| Aspek | Gaya lama (sebelum.cpp) | Gaya modern (sesudah.cpp) |
|---|---|---|
| Data | vector<Transaksi*> + new/delete | vector<Transaksi> berisi nilai; tidak ada alokasi manual |
| Konstruksi | constructor boilerplate (baris 13) | aggregate init dengan designated initializer {.id = ...} |
| Loop | indeks i + counter nomor | range-for + views::enumerate + structured bindings |
| Jumlahan | akumulator manual di dalam loop | fold_left di totalDari yang ter-constrain concept |
| Nilai terbesar | loop pembanding manual | std::ranges::max_element dengan projection |
| Konstanta | string literal tersebar | constexpr std::string_view di satu tempat |
| Keluaran | std::cout << ... << std::endl | std::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). autodan structured bindings — tipe ditulis sekali di sebelah kanan; destructuring membuat kode lebih pendek tanpa mengaburkan maksud.- Const correctness —
constpada variabel, parameterconst&, methodconst; perlakukanconstsebagai default, bukan tambahan. constexprdulu — nilai yang bisa diketahui saat kompilasi sebaiknya dihitung saat kompilasi; sisakan runtime untuk hal yang memang dinamis.- Tidak ada
new/deletemanual — container,std::string, dan smart pointer mengelola memori lewat RAII. - Algoritma dan views sebelum loop manual —
std::ranges::sort,find,fold_left, pipelinefilter | transformmenyatakan maksud, bukan mekanisme. - Concepts untuk syarat tipe — template yang memasang syarat eksplisit memberi error yang bisa dibaca (Chapter Concepts dan Ranges).
std::print/std::formatuntuk 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 .