Niat seorang penulis kode sering tidak terwakili dalam sintaks C++: ada fungsi yang kembaliannya wajib diperiksa, ada variabel yang memang sengaja tidak dipakai, ada penurunan antar-case yang disengaja, dan ada cabang yang jelas lebih sering dilalui daripada pasangannya. Tanpa sarana menyampaikan niat tersebut, compiler memperlakukan semua pola di atas sebagai kejadian biasa, dan pembaca kode harus menebak maksudnya. Attribute (atribut) adalah jawaban standar bahasa untuk kebutuhan ini: anotasi berbentuk [[nama]] yang menempel pada deklarasi atau pernyataan, berisi informasi tambahan bagi compiler tanpa mengubah arti program. Chapter fitur-cpp14 dan fitur-cpp17 sudah memperkenalkan dua anggotanya secara sekilas — [[deprecated]] dan [[nodiscard]]. Chapter ini merangkum perangkat atribut yang kita pakai sehari-hari:

  • [[nodiscard]] — nilai kembalian yang tidak boleh dibuang begitu saja.
  • [[maybe_unused]] — variabel atau parameter yang diamnya memang disengaja.
  • [[deprecated]] — API lama beserta pesan migrasi bagi pemanggilnya.
  • [[fallthrough]] — penurunan antar-case pada switch yang disengaja.
  • [[likely]] dan [[unlikely]] — perkiraan probabilitas cabang untuk optimasi.
  • [[no_unique_address]] — anggota kosong yang boleh berbagi alamat dengan anggota lain.

Kita mulai dari cara membaca atribut secara umum, lalu membuktikan setiap atribut dengan program teruji dan peringatan compiler yang nyata.

Atribut: Anotasi untuk Compiler, Bukan untuk Runtime #

Secara bentuk, atribut ditulis [[nama]] dan diletakkan di awal deklarasi atau sebelum pernyataan yang dimaksud; satu deklarasi boleh memuat beberapa atribut sekaligus. Secara semantik, atribut tidak menambah dan tidak mengurangi perilaku program: compiler hanya memanfaatkannya untuk memberi peringatan, mengatur optimasi, atau membantu diagnosis. Pembeda utamanya dari kata kunci bahasa ada di sini — program tetap bermakna dan tetap sah dikompilasi meski seluruh atributnya diabaikan.

Dari segi sumbernya ada dua keluarga. Atribut standar (misalnya [[nodiscard]]) tidak ber-namespace dan wajib dikenali setiap compiler modern; compiler yang belum mendukungnya wajib mengabaikannya, maksimal dengan memberi peringatan, sehingga kode tetap portabel ke compiler lama. Atribut khusus compiler (misalnya [[gnu::always_inline]] milik GCC dan [[clang::...]] milik Clang) selalu membawa nama namespace pemasoknya — pembahasannya kita simpan untuk bagian akhir chapter.

Program Pendalaman #

Program berikut mencicipi keenam atribut dalam satu penelusuran utuh — kompilasi bersih tanpa satu pun peringatan, dan setiap atribut ikut menentukan isi keluarannya:

cpp
// attributes-demo.cpp — tur ringkas atribut standar C++ pada deklarasi
// Kompilasi: g++ -std=c++23 -Wall -Wextra attributes-demo.cpp -o attributes-demo

#include <print>

struct Kosong {};

struct SimpanLengkap {
    int nilai;
    Kosong pelengkap;                         // anggota kosong dapat alamat sendiri
};

struct SimpanRapat {
    int nilai;
    [[no_unique_address]] Kosong pelengkap;   // boleh berbagi alamat dengan anggota lain
};

[[nodiscard("kode galat jangan dibuang")]] int muatBerkas(const char* nama) {
    return static_cast<int>(nama[0] == 'l' ? 0 : -1);
}

int jumlahkan(int a, int b) {
    [[maybe_unused]] const int metode = 2;    // hanya dibaca saat instrumentasi debug
    return a + b;
}

void terapkan(int kode, bool jalurIstimewa) {
    switch (kode) {
        case 1:
            std::println("kode satu");
            [[fallthrough]];                  // sengaja melanjutkan ke case 2
        case 2:
            std::println("jalur umum");
            break;
        default:
            std::println("kode lain");
            break;
    }
    if (jalurIstimewa) [[unlikely]] {
        std::println("jalur langka: perawatan khusus");
    } else [[likely]] {
        std::println("jalur utama: alur normal");
    }
}

int main() {
    // 1) [[nodiscard]] dipatuhi: nilai kembalian langsung disimpan
    const int kode = muatBerkas("laporan.txt");
    std::println("muatBerkas -> {}", kode);

    // 2) [[maybe_unused]] membuat build tetap bebas peringatan
    std::println("jumlahkan(2, 3) = {}", jumlahkan(2, 3));

    // 3) [[fallthrough]] + [[likely]]/[[unlikely]]
    terapkan(1, false);

    // 4) [[no_unique_address]]: ukuran struct dan alamat anggota kosong
    std::println("sizeof SimpanLengkap = {}", sizeof(SimpanLengkap));
    std::println("sizeof SimpanRapat   = {}", sizeof(SimpanRapat));
    SimpanRapat s{};
    const bool alamatBerbagi = reinterpret_cast<const void*>(&s.nilai)
                             == reinterpret_cast<const void*>(&s.pelengkap);
    std::println("alamat berbagi? {}", alamatBerbagi);
    return 0;
}

Output yang Dihasilkan:

text
muatBerkas -> 0
jumlahkan(2, 3) = 5
kode satu
jalur umum
jalur utama: alur normal
sizeof SimpanLengkap = 8
sizeof SimpanRapat   = 4
alamat berbagi? true

Mari kita bedah:

  1. Baris 6 (struct Kosong {}): tipe tanpa member sama sekali. Standar menjamin setiap objek punya alamat berbeda, sehingga Kosong tetap diberi ukuran 1 byte — titik awal untuk memahami [[no_unique_address]].
  2. Baris 8-16 (SimpanLengkap vs SimpanRapat): kedua struct menyimpan int plus anggota kosong pelengkap. Bedanya hanya satu: baris 15 memasang [[no_unique_address]] sehingga compiler diperbolehkan menumpuk pelengkap di atas alamat anggota lain — terbukti dari ukuran 8 versus 4.
  3. Baris 18-20 (muatBerkas): [[nodiscard]] membawa string alasan, bentuk C++20. Kembaliannya berupa kode galat — nilai yang tidak ada gunanya bila dibuang.
  4. Baris 23 ([[maybe_unused]] const int metode): variabel ini tidak dibaca pada build biasa; atributnya menyatakan diam itu disengaja sehingga -Wall -Wextra tetap tenang.
  5. Baris 31 ([[fallthrough]]): pernyataan kosong bertanda titik koma yang menyatakan penurunan dari case 1 ke case 2 memang disengaja, bukan break yang terlupa.
  6. Baris 39-43 ([[unlikely]]/[[likely]]): cabang langka diberi [[unlikely]], cabang utama [[likely]]. Eksekusi tetap sama tanpa keduanya — yang berubah hanya keputusan optimasi compiler.
  7. Baris 48 (pemanggilan muatBerkas): hasilnya disimpan ke kode, jadi kontrak [[nodiscard]] dipatuhi dan tidak ada peringatan.
  8. Baris 60-63 (pemeriksaan alamat): alamat kedua anggota s dibandingkan lewat reinterpret_cast ke const void*. Hasil true membuktikan [[no_unique_address]] benar-benar bekerja pada g++ 14.2.

[[nodiscard]] Menjaga Nilai Kembalian #

Atribut [[nodiscard]] (C++17, dengan string alasan sejak C++20) memasang kontrak pada nilai kembalian fungsi: pemanggil wajib memperhatikannya. Pustaka standar memakai atribut ini di tempat yang sering menjadi jebakan — std::async, yang temporary-nya menunggu pekerjaan selesai saat dihancurkan, dan std::optional::value() yang melempar std::bad_optional_access bila kosong. Kontrak itu nyata bagi compiler: bila hasilnya dibuang, g++ 14.2 bersuara. Kompilasi nyata dari g++ -std=c++23 -Wall -Wextra demo-nodiscard.cpp menghasilkan:

cpp demo-nodiscard.cpp
#include <string>

[[nodiscard("kode galat jangan dibuang")]] int muatBerkas(const std::string& nama) {
    return static_cast<int>(nama.size());
}

int main() {
    muatBerkas("laporan.txt");
}
text Peringatan g++ 14.2 — hasil kompilasi nyata
demo-nodiscard.cpp: In function ‘int main()’:
demo-nodiscard.cpp:8:15: warning: ignoring return value of ‘int muatBerkas(const std::string&)’, declared with attribute ‘nodiscard’: ‘kode galat jangan dibuang’ [-Wunused-result]
    8 |     muatBerkas("laporan.txt");
      |     ~~~~~~~~~~^~~~~~~~~~~~~~~
demo-nodiscard.cpp:3:48: note: declared here
    3 | [[nodiscard("kode galat jangan dibuang")]] int muatBerkas(const std::string& nama) {
      |                                                ^~~~~~~~~~

Pesan [[-Wunused-result]] menunjuk baris pemanggilan, mengutip tanda tangan fungsi, dan menyertakan alasan yang kita tulis sendiri — semacam dokumen migrasi mini yang muncul tepat di lokasi kesalahannya, ditambah catatan declared here yang menunjuk deklarasinya. Cara mengakhirinya ada dua: simpan nilainya seperti program utama di atas, atau nyatakan pembuangan disengaja dengan cast (void) — g++ 14.2 menutup peringatan untuk kasus kedua, meski compiler lain boleh tetap menyalakannya karena standar hanya menganjurkan peringatan, bukan mewajibkannya.

[[maybe_unused]] Menenangkan Variabel yang Sengaja Diam #

Peringatan -Wunused-variable dan -Wunused-parameter — pembahasan lengkapnya ada pada chapter Compiler Warnings — adalah alarm yang sehat, tetapi tidak setiap variabel diam adalah kelalaian. Variabel yang hanya dibaca saat assert() aktif, parameter yang dipakai di platform tertentu, atau penanda percobaan yang sengaja dipertahankan adalah kasus yang sah. [[maybe_unused]] (C++17) menyatakan hal itu tepat pada deklarasi. Tanpa atribut, variabel seperti itu membuat build kotor:

cpp demo-unused.cpp
int main() {
    const bool debugAktif = false;   // hanya dibaca saat build debug
}
text Peringatan g++ 14.2 — hasil kompilasi nyata
demo-unused.cpp: In function ‘int main()’:
demo-unused.cpp:2:16: warning: unused variable ‘debugAktif’ [-Wunused-variable]
    2 |     const bool debugAktif = false;   // hanya dibaca saat build debug
      |                ^~~~~~~~~~

Menambahkan satu kata [[maybe_unused]] di awal deklarasi yang sama membuat peringatan itu tidak muncul lagi — versi dengan atribut di sandbox penulis terkompilasi tanpa satu pun pesan. Ruang lingkup atribut ini luas: variabel lokal, parameter, member class, fungsi, hingga enumerator, dan memasangnya pada variabel yang ternyata dipakai tidak menimbulkan efek samping apa pun.

[[deprecated]] Membawa Pesan bagi Pemanggil #

Atribut [[deprecated]] (C++14) menandai API yang sebaiknya ditinggalkan: fungsi, class, member, enumerator, bahkan namespace seluruhnya. Target peringatannya bukan pemilik kode lama, melainkan setiap pemanggilnya — peringatan muncul di lokasi panggilan, lengkap dengan pesan yang kita tulis:

cpp demo-deprecated.cpp
[[deprecated("gunakan terapkanBaru() sejak API v2")]] void terapkanLama(int) { }

int main() {
    terapkanLama(7);
}
text Peringatan g++ 14.2 — hasil kompilasi nyata
demo-deprecated.cpp: In function ‘int main()’:
demo-deprecated.cpp:4:17: warning: ‘void terapkanLama(int)’ is deprecated: gunakan terapkanBaru() sejak API v2 [-Wdeprecated-declarations]
    4 |     terapkanLama(7);
      |     ~~~~~~~~~~~~^~~
demo-deprecated.cpp:1:60: note: declared here
    1 | [[deprecated("gunakan terapkanBaru() sejak API v2")]] void terapkanLama(int) { }
      |                                                            ^~~~~~~~~~~~

Flag [-Wdeprecated-declarations] ikut aktif bersama -Wall, sehingga pemanggil lama langsung terlihat saat build tanpa konfigurasi tambahan. Alur migrasi yang sehat memanfaatkannya bertahap: tandai API lama beserta penggantinya, beri masa transisi satu-dua rilis, lalu hapus penanda bersama kodenya — persis pola yang sudah kita cicipi pada chapter fitur-cpp14.

[[fallthrough]] Menegaskan Niat Turun Antar-case #

Penurunan eksekusi dari satu case ke case berikutnya (fallthrough) adalah salah satu sumber bug tertua di C: break yang terlupa membuat program melanjutkan ke logika yang tidak disengaja. Karena itu -Wextra mengaktifkan -Wimplicit-fallthrough untuk setiap penurunan yang tidak diberi keterangan. Bila penurunan memang disengaja, kita tidak perlu membengkokkan alur demi membungkam peringatan — cukup nyatakan niatnya:

cpp demo-fallthrough.cpp — tanpa penanda
#include <print>

void terapkan(int kode) {
    switch (kode) {
        case 1:
            std::println("kode satu");
        case 2:
            std::println("jalur umum");
            break;
        default:
            std::println("kode lain");
    }
}
text Peringatan g++ 14.2 — hasil kompilasi nyata
demo-fallthrough.cpp: In function ‘void terapkan(int)’:
demo-fallthrough.cpp:6:25: warning: this statement may fall through [-Wimplicit-fallthrough=]
    6 |             std::println("kode satu");
      |             ~~~~~~~~~~~~^~~~~~~~~~~~~
demo-fallthrough.cpp:7:9: note: here
    7 |         case 2:
      |         ^~~~

Menyisipkan [[fallthrough]]; — perhatikan titik komanya, atribut ini berbentuk pernyataan kosong — tepat sebelum label berikutnya membuat peringatan yang sama tidak muncul lagi, sementara perilaku program tidak berubah sedikit pun. Posisinya juga informatif bagi pembaca: baris case 1 yang berakhir tanpa break kini terbaca sebagai keputusan yang diambil, bukan kelalaian.

[[likely]] dan [[unlikely]] Memberi Hint pada Cabang #

Dua atribut C++20 ini menyatakan perkiraan kita tentang seberapa sering sebuah jalur dilalui. Letaknya tepat setelah kondisi if, setelah else, atau pada label case:

cpp Dua penempatan umum
if (jalurIstimewa) [[unlikely]] {
    // jalur langka
} else [[likely]] {
    // jalur utama
}

switch (mode) {
    case MODE_UMUM: [[likely]]
        prosesUmum();
        break;
    default:
        prosesLain();
}

Maknanya terbatas tetapi berguna: compiler dianjurkan mengoptimalkan seolah jalur yang menyertakan atribut lebih (atau kurang) mungkin dilalui — biasanya lewat penataan ulang blok kode dan instruksi prediksi cabang. Standar menyebutnya recommendation: compiler bebas mengabaikannya, dan hint yang salah justru bisa menurunkan performa. Praktik yang sehat adalah memasangnya berdasarkan data profiling (misalnya hasil pasangan flag -fprofile-generate/-fprofile-use), bukan berdasarkan perasaan; jalur yang diberi [[unlikely]] sebaiknya memang jarang terjadi dan murah bila diperlakukan salah.

[[no_unique_address]] Memadatkan Anggota Kosong #

Banyak class modern menyimpan objek kebijakan (policy) yang tidak punya state: comparator std::less<int>, allocator kosong, atau deleter kustom. Objek semacam itu berukuran 1 byte dan tetap mendapat alamat sendiri, sehingga struct penampungnya membengkak dari padding, bukan dari data. [[no_unique_address]] (C++20) memberi izin kepada compiler untuk menumpuk anggota kosong di atas alamat anggota lain yang bertipe berbeda:

cpp Pola: anggota kebijakan yang rapat
struct Penampung {
    [[no_unique_address]] std::less<int> banding;   // kosong, tanpa state
    std::vector<int> data;
};
// g++ 14.2 (terverifikasi lewat static_assert):
// sizeof(Penampung) == sizeof(std::vector<int>) == 24

Program utama chapter ini membuktikan efeknya dengan angka: SimpanRapat berukuran 4 byte, setengah dari SimpanLengkap yang 8 byte, dan kedua alamat anggotanya sama. Dua batasan perlu diingat. Penumpukan hanya terjadi antara tipe yang berbeda — dua anggota bertipe sama tetap mendapat alamat unik agar identitas objek terjaga — dan dukungan compiler bervariasi, jadi uji lewat sizeof bila penghematan byte ini penting bagi desain Anda.

Atribut Standar versus Ekstensi Compiler #

Aturan pembagiannya tegas: atribut standar ditulis tanpa namespace, atribut pemasok wajib ber-namespace (gnu:: untuk GCC, clang:: untuk Clang). Compiler wajib mengabaikan atribut ber-namespace yang tidak dikenal — dan boleh memberi tahu kita lewat peringatan. Inilah yang terjadi bila kita salah menulis namespace, hasil kompilasi nyata g++ 14.2:

cpp demo-unknown.cpp
[[tersembunyi::rahasia]] int misterius() { return 1; }
text Peringatan g++ 14.2 — hasil kompilasi nyata
probe2.cpp:2:40: warning: ‘tersembunyi::rahasia’ scoped attribute directive ignored [-Wattributes]
    2 | [[tersembunyi::rahasia]] int misterius() { return 1; }
      |                                        ^

Selain keluarga ekstensi pemasok, beberapa atribut standar lain layak Anda kenali sejak sekarang:

AtributSejakFungsi singkat
[[noreturn]]C++11menandai fungsi yang tidak pernah kembali ke pemanggil (throw, std::exit)
[[carries_dependency]]C++11menyatakan dependensi memori atomik pada parameter atau kembalian
[[assume(expr)]]C++23menyatakan asumsi fakta bagi optimasi, berbentuk pernyataan [[assume(x > 0)]];

Strategi portabilitas yang kita anjurkan: jadikan atribut standar pilihan pertama, dan simpan ekstensi pemasok untuk kebutuhan yang benar-benar khusus — bungkus dengan pemeriksaan macro compiler (#if defined(__GNUC__)) agar build di platform lain tetap sah. Karena compiler wajib mengabaikan atribut yang tidak dikenal, kesalahan kecil hanya berujung peringatan, bukan kegagalan build.

Atribut adalah salah satu bentuk berbicara kepada compiler saat kompilasi. Chapter berikutnya membahas bentuk yang lebih kuat: if constexpr, percabangan yang dievaluasi compiler dan menentukan kode mana yang layak ikut dikompilasi — alat yang pada chapter lambda sudah kita lihat sekilas di dalam template.

Rujukan resmi: cppreference — attributes , GCC — Attribute Syntax .