Pada Chapter struct kita sempat menyisipkan satu baris Produk gula {"Gula Pasir", 40, 14000.0}; tanpa penjelasan panjang — pola itu bernama aggregate initialization, dan sekarang waktunya membukanya tuntas. Membuat struct lalu mengisi member satu per satu memang sah, tetapi rawan lupa: satu member yang tertinggal membuat objek berjalan dengan nilai sampah. Aggregate initialization mengisi seluruh member dalam satu pernyataan, dan sejak C++20 kita bahkan bisa menuliskan nama membernya secara eksplisit lewat designated initializers. Pada Chapter ini kita membedah inisialisasi penuh, parsial, kosong, bertingkat, serta syarat yang membuat sebuah struct layak disebut aggregate.
Inisialisasi dengan Daftar Nilai #
Sebuah struct tanpa constructor, tanpa member private, dan tanpa fungsi virtual disebut aggregate — “kumpulan” member polos. Aggregate boleh diinisialisasi dengan daftar nilai di dalam {}, diisi berurutan sesuai urutan deklarasi member. Dari daftar itu pula lahir tiga varian yang sering dipakai: penuh, parsial, dan kosong.
#include <iostream>
#include <string>
struct Titik {
int x;
int y;
int z;
};
struct Garis {
Titik awal;
Titik akhir;
std::string nama;
};
void cetak(const char* label, const Titik& t) {
std::cout << label << ": (" << t.x << ", " << t.y << ", " << t.z << ")\n";
}
int main() {
Titik p1 {2, 5, 8}; // penuh: tiga member, tiga nilai
Titik p2 {7}; // parsial: x = 7, sisanya nol
Titik p3 {}; // kosong: semua member bernilai nol
Titik p4 {.x = 1, .y = 4, .z = 9}; // designated initializer (C++20)
Titik p5 {.z = 6}; // designated: hanya member z
Garis diagonal {{1, 2, 3}, {4, 5, 6}, "diagonal-utama"}; // nested init
cetak("p1", p1);
cetak("p2", p2);
cetak("p3", p3);
cetak("p4", p4);
cetak("p5", p5);
std::cout << diagonal.nama << ": (" << diagonal.awal.x << ", " << diagonal.awal.y
<< ", " << diagonal.awal.z << ") -> (" << diagonal.akhir.x << ", "
<< diagonal.akhir.y << ", " << diagonal.akhir.z << ")\n";
return 0;
}Output yang Dihasilkan:
p1: (2, 5, 8)
p2: (7, 0, 0)
p3: (0, 0, 0)
p4: (1, 4, 9)
p5: (0, 0, 6)
diagonal-utama: (1, 2, 3) -> (4, 5, 6)Mari kita telaah apa yang terjadi pada baris-baris kode di atas:
- Baris 21 (
Titik p1 {2, 5, 8};): inisialisasi penuh. Nilai2untukx,5untuky,8untukz— urutannya mengikuti urutan deklarasi member, bukan nama apa pun. - Baris 22 (
Titik p2 {7};): inisialisasi parsial.xmenerima7, sedangkanydanzdiisi nol oleh compiler — bukan nilai sampah. Aturan inilah yang membedakan inisialisasi dengan{}dari deklarasi tanpa inisialisasi. - Baris 23 (
Titik p3 {};): inisialisasi kosong yang disebut zero-initialization; seluruh member menjadi nol. PolaTitik p3 {};adalah cara tercepat memastikan struct bersih. - Baris 25-26: designated initializers C++20 — member dituliskan namanya dengan
.nama = nilai, sehingga pembaca langsung melihat nilai itu untuk member mana. - Baris 28: nested initialization. Member
awaldanakhiryang bertipeTitikdiisi dengan daftar{}di dalam daftar induk, dannamadiisi string.
Dengan flag -Wall -Wextra yang kita pakai sejak awal, compiler menegur dua baris pada contoh di atas. Inilah pesan peringatannya:
warning: missing initializer for member 'Titik::y' [-Wmissing-field-initializers]
warning: missing initializer for member 'Titik::z' [-Wmissing-field-initializers]Designated Initializers C++20 #
Sintaks .nama = nilai menyelesaikan satu kelemahan inisialisasi berurutan: pembaca tidak perlu membuka definisi struct untuk tahu arti angka ketiga. Perbandingan berikut menunjukkan keunggulannya:
Titik a {320, 240, 16}; // 320 untuk apa? Harus buka definisi Titik
Titik b {.x = 320, .y = 240, .z = 16}; // maksud tiap nilai terlihat langsung
Ada satu aturan tegas yang berbeda dari bahasa C: designated initializers pada C++ harus mengikuti urutan deklarasi member. Boleh melewatkan member, tetapi tidak boleh menulis member yang terdeklarasi belakangan sebelum yang awal. Compiler menolak pelanggarannya dengan pesan yang jelas:
error: designator order for field 'Titik::x' does not match declaration order in 'Titik'Pesan di atas lahir dari Titik p {.y = 1, .x = 2}; — x dideklarasikan sebelum y, sehingga .y tidak boleh muncul lebih dulu. Aturan ini menjaga definisi inisialisasi tetap tunggal: compiler tidak perlu menebak dua cara pengurutan yang mungkin.
Syarat Aggregate #
Sifat “boleh diisi dengan daftar {}” bukan hak semua tipe. Syarat utamanya untuk struct: tidak ada constructor yang dideklarasikan sendiri, tidak ada member non-public, tidak ada fungsi virtual, dan tidak ada base class. Begitu salah satunya hadir, tipe itu bukan lagi aggregate dan daftar {} dianggap sebagai argumen constructor. Contoh berikut adalah struct dengan constructor yang ditulis sendiri — konsep constructor sendiri kita pelajari mendalam di Section Pemrograman Berorientasi Objek, tetapi inti permasalahannya sudah bisa kita nikmati sekarang:
struct Rekening {
std::string pemilik;
int saldo;
Rekening(std::string p) : pemilik(p), saldo(0) {} // kehadiran constructor
}; // mencabut status aggregate
int main() {
Rekening r {"Budi", 5000}; // tidak lagi diisi lewat daftar member
return 0;
}Kompilasi kode di atas ditolak dengan pesan:
error: no matching function for call to 'Rekening::Rekening(<brace-enclosed initializer list>)'Compiler mencari constructor yang menerima dua argumen dan tidak menemukannya, karena satu-satunya constructor yang ada hanya menerima satu. Sebaliknya, struct Titik dan Garis pada contoh utama memenuhi semua syarat aggregate, sehingga daftar {} diperlakukan sebagai pengisi member. Kelak ketika kita menulis class dengan constructor, perbedaan perilaku ini akan terasa wajar: tipe yang mengelola pembuatan dirinya sendiri menentukan sendiri bentuk argumennya.
Referensi resmi untuk topik ini dapat dibaca pada halaman aggregate initialization di cppreference. Selanjutnya kita menumpuk struct di dalam struct: bagaimana tipe komposit boleh beranggotakan tipe komposit lain, dan apa bedanya mendefinisikan nested struct di dalam atau di luar induknya.