noexcept

Janji tanpa exception dan perannya pada operasi move.

Daftar isi

Kata kunci noexcept menandai fungsi yang dijanjikan tidak melempar exception dan sekaligus menjadi operator yang menguji janji tersebut saat kompilasi. Topik ini membahas kedua peran tersebut, perilaku program ketika janji dilanggar, serta peran janji ini pada operasi move dan realokasi container. Program contoh di topik ini mencetak hasil pengujiannya, sehingga hubungan antara deklarasi fungsi dan janji yang diberikannya dapat diperiksa langsung.

Janji tanpa exception#

Deklarasi noexcept pada suatu fungsi merupakan janji bahwa exception tidak akan keluar dari badannya. Apabila exception tetap mencoba keluar, misalnya karena badannya memanggil fungsi lain yang melempar, pencarian penangan tidak dilakukan dan std::terminate langsung dipanggil, sehingga programnya berhenti. Perilaku ini berbeda dari fungsi biasa, yang meneruskan exception kepada pemanggilnya. Perbedaan tersebut tidak terlihat pada kode pemanggil sama sekali.

Janji ini juga melekat pada fungsi tertentu secara bawaan. Destructor, termasuk destructor kelas buatan sendiri, tidak melempar exception kecuali ditandai lain, dan aturan ini berlaku sejak C++11. Bentuk lama penandaan berupa throw() sudah tidak dipakai lagi pada standar yang lebih baru, sehingga noexcept menjadi satu-satunya bentuk yang perlu ditulis untuk janji semacam ini.

noexcept sebagai operator#

Selain sebagai spesifier, noexcept dapat ditulis sebagai operator dengan bentuk noexcept(ekspresi). Operator ini tidak mengeksekusi ekspresinya, melainkan menguji saat kompilasi apakah ekspresi tersebut dinyatakan tidak melempar exception, lalu menghasilkan nilai kebenaran. Hasilnya dapat dicetak, disimpan dalam variabel constexpr, atau digabungkan dengan pustaka <type_traits> seperti std::is_nothrow_move_constructible_v untuk menguji sebuah tipe.

Program berikut menguji dua fungsi biasa dan dua kelas yang move constructor-nya berbeda janjinya.

cpp uji-noexcept.cpp
#include <iostream>
#include <type_traits>

void biasa() {}
void aman() noexcept {}

// move constructor diberi janji noexcept, copy constructor tidak
class KotakAman {
public:
    KotakAman(const KotakAman&) {}
    KotakAman(KotakAman&&) noexcept {}
};

// move constructor tanpa janji noexcept
class KotakLain {
public:
    KotakLain(const KotakLain&) {}
    KotakLain(KotakLain&&) {}
};

int main() {
    std::cout << "noexcept(biasa()) = " << noexcept(biasa()) << "\n";
    std::cout << "noexcept(aman()) = " << noexcept(aman()) << "\n";
    std::cout << "move KotakAman noexcept = "
              << std::is_nothrow_move_constructible_v<KotakAman> << "\n";
    std::cout << "move KotakLain noexcept = "
              << std::is_nothrow_move_constructible_v<KotakLain> << "\n";
    return 0;
}
text Keluaran
noexcept(biasa()) = 0
noexcept(aman()) = 1
move KotakAman noexcept = 1
move KotakLain noexcept = 0

Dua baris pertama menunjukkan bahwa janji hanya mengikuti deklarasi: biasa() tanpa noexcept menghasilkan 0, sedangkan aman() yang ditandai noexcept menghasilkan 1, dan keduanya dinilai tanpa membaca isinya. Dua baris berikutnya menunjukkan hasil yang sama pada tingkat tipe. KotakAman yang move constructor-nya noexcept dilaporkan sebagai tipe yang dapat dipindah tanpa exception, sedangkan KotakLain tidak, meskipun kedua badannya sama-sama kosong.

noexcept dan operasi move#

Nilai terbesar janji noexcept terletak pada operasi move. Saat std::vector tumbuh melebihi kapasitasnya, seluruh elemen lama harus dipindahkan ke blok memori yang baru. Proses ini hanya dapat memberi jaminan strong apabila pemindahannya tidak mungkin gagal di tengah jalan, karena kegagalan di tengah realokasi akan merusak elemen lama yang sudah mulai dipindahkan. Karena itu vector menggunakan move constructor ketika move tersebut noexcept. Apabila move tidak noexcept, vector menyalin setiap elemen dengan copy constructor agar elemen lama tetap utuh saat exception terjadi.

Pemilihan antara move dan copy pada situasi ini dilakukan oleh std::move_if_noexcept, yaitu utilitas yang menghasilkan rvalue hanya untuk tipe yang pemindahannya noexcept atau yang sama sekali tidak dapat disalin. Konsekuensi praktisnya sederhana: move constructor dan move assignment untuk tipe yang akan tersimpan dalam container sebaiknya ditandai noexcept. Tanpa janji tersebut, container diam-diam beralih ke penyalinan dan keuntungan performa move semantics hilang tanpa pesan apa pun.

Pola yang sama berlaku pada operasi container lain yang memindahkan elemen, misalnya std::vector::resize dan penyisipan yang memicu realokasi. Selama move constructor tipe elemennya noexcept, pustaka standar boleh memindahkan elemen, sedangkan apabila janji itu tidak ada, jalur penyalinan yang lebih mahal dipilih demi keandalan. Pemeriksaan trait seperti pada program sebelumnya memungkinkan klaim ini diverifikasi sebelum kelas benar-benar dipakai dalam container.

Kapan janji diberikan#

Urutan keputusannya dapat dirangkum sebagai berikut.

  1. Tandai noexcept pada move constructor, move assignment, dan fungsi swap, karena ketiganya menjadi fondasi realokasi dan pertukaran yang aman.
  2. Tandai noexcept pada fungsi daun yang benar-benar tidak memanggil kode yang dapat melempar, misalnya fungsi aritmetika murni atas tipe fundamental.
  3. Tunda penandaan pada fungsi yang mengalokasi memori, melakukan I/O, atau memanggil pustaka eksternal, karena badannya dapat melempar std::bad_alloc dan exception lain.

Janji ini juga merupakan bagian dari antarmuka fungsi, sehingga perubahannya dirasakan oleh pengguna kodenya. Menambah noexcept setelah fungsi dirilis dapat mematahkan kode pemanggil yang mengandalkan exception yang dilempar fungsi tersebut, sedangkan menghapusnya hanya membuat program kehilangan peluang optimasi. Karena itu penandaan sebaiknya dipikirkan sejak perancangan, khususnya pada tipe yang dikelola container.

Kesalahan umum#

  • Menandai fungsi noexcept padahal badannya dapat melempar exception, misalnya karena memanggil fungsi lain yang melempar. Program tetap lolos kompilasi, tetapi berhenti di std::terminate saat exception terjadi. Perbaikannya dengan mengaudit seluruh panggilan di badan fungsi sebelum janji diberikan.
  • Mengira noexcept mengubah perilaku penanganan exception. Janji ini tidak menyediakan penangan apa pun, melainkan menyatakan bahwa penangan tidak dibutuhkan. Perbaikannya dengan memakai try dan catch pada titik yang memang harus menangani error.
  • Menandai move constructor noexcept tanpa memeriksa membernya. Janji hanya sah apabila seluruh langkah di badannya aman, termasuk pemindahan setiap membernya. Perbaikannya dengan memberi janji pada member yang relevan terlebih dahulu, lalu memverifikasi janji kelasnya lewat std::is_nothrow_move_constructible_v.

Ringkasan#

  • noexcept menandai fungsi yang tidak melempar exception, dan pelanggarannya berakhir di std::terminate, bukan di penangan exception.
  • Sebagai operator, noexcept(ekspresi) menguji janji tersebut saat kompilasi dan menghasilkan nilai yang dapat dicetak atau digabungkan dengan <type_traits>.
  • std::vector menggunakan move constructor hanya ketika move tersebut noexcept, sehingga penandaan ini menentukan apakah move semantics benar-benar dimanfaatkan saat realokasi.

Referensi#