Sebelum C++11, C++ punya empat cara inisialisasi dengan aturan berbeda-beda: int a = 5;, int arr[] = {1,2,3};, Poin p(3,4);, dan member initializer list : x(3). Masing-masing tidak saling tertukar — vector bisa diisi daftar elemen, struct biasa tidak; ada tipe yang butuh (, ada yang butuh =, dan ada kasus sintaks Jam j(); yang bukan inisialisasi sama sekali. C++11 memperkenalkan uniform initialization dengan kurung kurawal {}: satu sintaks untuk semua kategori tipe, dengan dua hadiah tambahan — penolakan narrowing conversion dan berakhirnya most vexing parse. Chapter ini membedah keduanya beserta satu jebakan yang paling sering mengejutkan pemula: constructor std::initializer_list yang selalu menang.

Satu Sintaks untuk Semua Tipe #

cpp
// uniform-init.cpp — satu sintaks {} untuk semuanya, plus jebakan yang wajib dikenali
// Kompilasi: g++ -std=c++23 -Wall -Wextra uniform-init.cpp -o uniform-init

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

struct Poin {          // aggregate: struct biasa tanpa constructor
    int x;
    int y;
};

struct Jam {
    int detik = 0;
    explicit Jam(int d) : detik(d) {}
    Jam() = default;
};

struct Paket {         // punya DUA constructor: dipilih sesuai bentuk {}
    Paket(int a, int b) {
        std::cout << "Paket: ctor 2 argumen (" << a << "," << b << ")\n";
    }
    Paket(std::initializer_list<int> li) {
        std::cout << "Paket: ctor initializer_list (" << li.size() << " elemen)\n";
    }
};

void tampil(const std::string& label, const std::vector<int>& v) {
    std::cout << label << " -> " << v.size() << " elemen:";
    for (int x : v) std::cout << " " << x;
    std::cout << "\n";
}

int main() {
    // satu sintaks untuk semua kategori tipe
    int a{5};
    double pi{3.14};
    std::string kata{"modern"};
    Poin p{3, 4};                     // aggregate init
    std::vector<std::string> nama{"budi", "dewi", "siti"};
    Jam j{45};                        // explicit constructor tetap dipakai

    std::cout << "a=" << a << " pi=" << pi << " kata=" << kata << "\n";
    std::cout << "p=(" << p.x << "," << p.y << ") "
              << "nama[0]=" << nama[0] << " jam=" << j.detik << "\n";

    Jam defaultJam{};                 // value-initialization -> member default
    std::cout << "jam kosong=" << defaultJam.detik << "\n";

    // initializer_list menang atas constructor lain saat ditulis {}
    std::vector<int> v1{3};           // initializer_list: 1 elemen bernilai 3
    std::vector<int> v2(3);           // constructor count : 3 elemen bernilai 0
    std::vector<int> v3(3, 7);        // constructor count+nilai
    std::vector<int> v4{3, 7};        // initializer_list: 2 elemen {3, 7}

    tampil("v1{3}  ", v1);
    tampil("v2(3)  ", v2);
    tampil("v3(3,7)", v3);
    tampil("v4{3,7}", v4);

    std::cout << "-- pemilihan constructor pada Paket --\n";
    Paket p1{1, 2};   // initializer_list lebih "cocok", ia yang dipilih
    Paket p2(1, 2);   // kurung memaksa ctor 2 argumen
    return 0;
}

Output yang Dihasilkan:

text
a=5 pi=3.14 kata=modern
p=(3,4) nama[0]=budi jam=45
jam kosong=0
v1{3}   -> 1 elemen: 3
v2(3)   -> 3 elemen: 0 0 0
v3(3,7) -> 3 elemen: 7 7 7
v4{3,7} -> 2 elemen: 3 7
-- pemilihan constructor pada Paket --
Paket: ctor initializer_list (2 elemen)
Paket: ctor 2 argumen (1,2)

Mari kita bedah:

  1. Baris 27-32: skalar, string, aggregate, container, dan class berconstructor semuanya diisi dengan {}. Inilah “uniform”: satu bentuk mental untuk semua kasus.
  2. Baris 28 (Poin p{3, 4}): inisialisasi aggregate — compiler memasangkan 3 ke x dan 4 ke y secara berurutan tanpa constructor. C++20 menambah varian dengan nama member: Poin p{.x = 3, .y = 4}.
  3. Baris 35 (Jam defaultJam{}): {} tanpa isi memicu value-initialization — member yang punya default (int detik = 0) diisi nilai bawaannya. Kontras dengan int x; tanpa inisialisasi yang berisi nilai acak.
  4. Baris 38-41 (jebakan utama): v1{3} membuat vector berisi satu elemen bernilai 3, bukan tiga elemen kosong. Yang membuat tiga elemen adalah v2(3) dengan tanda kurung. Ini perbedaan yang paling sering menimbulkan bug halus.
  5. Baris 52-54 (Paket): bila class punya constructor initializer_list, bentuk {...} selalu memilihnya — meski ada constructor yang lebih pas argumennya. Hanya tanda kurung (...) yang bisa memaksakan constructor biasa.

{} Menolak Narrowing #

Narrowing conversion adalah penyusutan nilai yang bisa kehilangan data — double ke int, long long ke int, int ke char. Sintaks lama menerimanya diam-diam; {} menolaknya saat kompilasi:

cpp
int salah{3.14};
// error: narrowing conversion of '3.1400000000000001e+0'
//        from 'double' to 'int' [-Wnarrowing]

char huruf{300};
// error: narrowing conversion of '300' from 'int' to 'char' [-Wnarrowing]

int ok = 3.14;   // sintaks lama: lolos, salah berubah diam-diam menjadi 3

Pesan error pertama persis seperti yang dihasilkan g++ 14.2. Konversi yang aman (misalnya int x{5}; dari literal 5 yang diketahui pas) tetap diperbolehkan, sehingga aturan ini mencegah kehilangan data tanpa mengganggu kasus normal.

Most Vexing Parse Terselesaikan #

Ada kasus legendaris di C++: baris yang kita maksud “buat objek” tetapi compiler membacanya “deklarasikan fungsi”. Dengan sintaks kurung, bahkan compiler pun harus menebak — dan g++ kini memberi peringatan khusus bernama -Wvexing-parse:

text Peringatan compiler: empty parentheses were disambiguated as a function declaration
warning: empty parentheses were disambiguated as a function declaration [-Wvexing-parse]
    4 |     Jam j();        // PENGECAT: deklarasi fungsi bernama j!
      |          ^~
note: remove parentheses to default-initialize a variable
note: or replace parentheses with braces to aggregate-initialize a variable

Jam j(); adalah deklarasi fungsi j yang mengembalikan Jam — bukan objek. Dengan {}, keambiguan itu hilang karena kurung kurawal tidak punya makna deklarasi fungsi:

cpp
Jam j();    // deklarasi fungsi (dengan peringatan compiler)
Jam j{};    // objek Jam — pasti, tanpa keambiguan

Aturan praktisnya: pakai {} sebagai bawaan (default) untuk inisialisasi, karena ia menyatukan semua tipe, menolak penyusutan nilai, dan bebas dari parse berbahaya; pakai (...) hanya saat maksud Anda persis memilih constructor non-initializer_list, seperti std::vector<int> v(1000).

Dengan fondasi inisialisasi yang sehat, langkah berikutnya masuk ke sisi lain dari sama koinnya: bagaimana compiler menyimpulkan tipe dari inisialisasi itu — topik penuh auto, decltype, dan aturan deduksinya di chapter berikutnya.

Rujukan resmi: cppreference — list initialization , cppreference — initializer_list .