Pada Chapter Exception Safety Guarantees, noexcept muncul sebagai jaminan paling kuat: operasi yang tidak pernah melempar. Sekarang kita membahasnya sebagai bahasa. Keyword noexcept punya dua wajah yang sering ditukar: sebagai specifier ia adalah janji yang kita tempelkan pada deklarasi function, dan sebagai operator ia adalah pertanyaan yang dievaluasi compile time — noexcept(f()) bernilai true bila ekspresi itu dijamin tidak melempar. Janji ini ternyata bukan sekadar dokumentasi: pustaka standar membacanya untuk memilih strategi, dan contoh paling terkenal adalah std::vector yang hanya berani memakai move saat realokasi bila move constructor-nya noexcept.
noexcept Specifier: Janji yang Diperiksa Compiler #
Menempelkan noexcept pada deklarasi function berarti berjanji: function ini tidak akan melempar exception apa pun ke luar. Compiler tidak memverifikasi badan function baris demi baris, tetapi ia membaca janji itu untuk optimasi dan untuk memperingatkan pelanggaran yang kasat mata. Apa yang terjadi bila janji dilanggar? Mari kita lihat file terminate-demo.cpp yang function-nya berjanji noexcept lalu langsung melempar:
#include <stdexcept>
void apel() noexcept { // berjanji tidak melempar...
throw std::runtime_error("melanggar janji");
}
int main() {
apel(); // ...compiler tetap memanggilnya
return 0;
}Saat mengompilasi, GCC 14 sudah menyindir pelanggaran yang terang ini (hasil nyata):
terminate-demo.cpp: In function ‘void apel()’:
terminate-demo.cpp:4:5: warning: ‘throw’ will always call ‘terminate’ [-Wterminate]
4 | throw std::runtime_error("melanggar janji");
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Dan bila program itu tetap dijalankan, runtime memutar janji yang dilanggar menjadi penghentian program — bukan pengecualian yang bisa ditangkap (eksekusi nyata; program berakhir dengan sinyal SIGABRT, exit code 134):
terminate called after throwing an instance of 'std::runtime_error'
what(): melanggar janjiPesan terminate called after throwing... berasal dari std::terminate(): mekanisme yang dipanggil runtime saat exception melewati fungsi berjanji noexcept. Tidak ada catch di dunia ini yang bisa menyelamatkannya — program langsung berakhir. Karena itu kontraknya jelas: pakai noexcept hanya untuk function yang benar-benar tidak boleh gagal, bukan untuk “semoga saja tidak gagal”.
Satu kelas function yang secara implicit sudah berjanji noexcept adalah destructor. Destruktor yang tidak diberi anotasi apa pun dianggap noexcept secara default — dan memang seharusnya begitu, karena destructor berjalan saat unwinding, saat tidak ada tempat aman lagi untuk menangani exception baru. Menulis throw di dalam destructor adalah pola yang harus dihindari; bila benar-benar diperlukan (hampir tidak pernah), function itu harus dideklarasikan eksplisit noexcept(false).
noexcept Operator dan static_assert #
Wajah kedua noexcept adalah operator: noexcept(ekspresi) dievaluasi sepenuhnya di compile time dan menghasilkan konstanta true/false. Ini alat pemeriksaan, bukan alat eksekusi — ekspresinya tidak dijalankan. Kombinasi favoritnya adalah dengan type traits <type_traits>: std::is_nothrow_move_constructible_v<T> menanyakan apakah move constructor T berjanji tidak melempar, persis properti yang dibaca std::vector.
Program berikut memeriksa semuanya lewat static_assert (Chapter Compile-time vs Runtime Errors), lalu membuktikan akibatnya di runtime: dua class yang identik kecuali move constructor satu di antaranya noexcept, di-push_back tiga kali ke masing-masing vector:
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
void aman() noexcept;
void berisiko();
// Versi 1: move constructor LUPA noexcept.
class SalinSaatRealokasi {
public:
explicit SalinSaatRealokasi(std::string nama) : nama_(std::move(nama)) {}
SalinSaatRealokasi(const SalinSaatRealokasi& lain) : nama_(lain.nama_) {
std::cout << " copy\n";
}
SalinSaatRealokasi(SalinSaatRealokasi&& lain) : nama_(std::move(lain.nama_)) {
std::cout << " move\n";
}
private:
std::string nama_;
};
// Versi 2: move constructor noexcept.
class MoveSaatRealokasi {
public:
explicit MoveSaatRealokasi(std::string nama) : nama_(std::move(nama)) {}
MoveSaatRealokasi(const MoveSaatRealokasi& lain) : nama_(lain.nama_) {
std::cout << " copy\n";
}
MoveSaatRealokasi(MoveSaatRealokasi&& lain) noexcept
: nama_(std::move(lain.nama_)) {
std::cout << " move\n";
}
private:
std::string nama_;
};
// static_assert memeriksa kontrak saat compile time, bukan saat berjalan.
static_assert(std::is_nothrow_move_constructible_v<MoveSaatRealokasi>,
"move constructor wajib noexcept agar vector memakai move");
static_assert(!std::is_nothrow_move_constructible_v<SalinSaatRealokasi>,
"versi tanpa noexcept memang tidak nothrow");
static_assert(std::is_nothrow_destructible_v<MoveSaatRealokasi>,
"destructor implicit selalu noexcept");
int main() {
// noexcept sebagai OPERATOR: menanyakan ekspresi pada compile time.
std::cout << "noexcept(aman()) = " << noexcept(aman()) << "\n";
std::cout << "noexcept(berisiko()) = " << noexcept(berisiko()) << "\n";
std::cout << "vector<SalinSaatRealokasi>: 3x push_back\n";
std::vector<SalinSaatRealokasi> v1;
for (int i = 0; i < 3; ++i) {
v1.push_back(SalinSaatRealokasi{"elemen"});
}
std::cout << "vector<MoveSaatRealokasi>: 3x push_back\n";
std::vector<MoveSaatRealokasi> v2;
for (int i = 0; i < 3; ++i) {
v2.push_back(MoveSaatRealokasi{"elemen"});
}
return 0;
}Output yang Dihasilkan:
noexcept(aman()) = 1
noexcept(berisiko()) = 0
vector<SalinSaatRealokasi>: 3x push_back
move
move
copy
move
copy
copy
vector<MoveSaatRealokasi>: 3x push_back
move
move
move
move
move
moveYang terjadi di balik output itu:
- Baris 57-58 (operator
noexcept):noexcept(aman())bernilai1karena deklarasinya berjanji;noexcept(berisiko())bernilai0karena tanpa janji. Tidak ada function yang dipanggil di sini — hanya pemeriksaan compile time yang dicetak. - Baris 54-60 (
static_assert): tiga pemeriksaan type traits yang lolos kompilasi, membuktikanMoveSaatRealokasinothrow-moveable,SalinSaatRealokasitidak, dan destructor keduanyanoexceptsecara implicit. - Output
vector<SalinSaatRealokasi>: tigapush_backmemicu dua realokasi (kapasitas 1 → 2 → 4). Karena move constructor tidak berjanjinoexcept, vector mengamankan elemen lama dengan copy — total tigamoveuntuk elemen baru dan tigacopyuntuk relokasi. - Output
vector<MoveSaatRealokasi>: enammove, nolcopy. Realokasi memindahkan elemen lama alih-alih menyalinnya — lebih murah, dan perilakunya konsisten di semua ukuran. - Urutan persis jejak cetak adalah milik implementasi libstdc++ (elemen baru dibangun lebih dulu, lalu elemen lama direlokasi); yang dijamin standar adalah pilihannya: move saat
noexcept, copy bila tidak.
Mengapa vector begitu berhati-hati? Karena realokasi dengan move yang bisa melempar tidak mungkin memberi jaminan kuat: bila move elemen ke-tiga gagal di tengah jalan, elemen satu dan dua sudah dipindahkan — sumbernya sudah kosong — dan keadaan vector tak bisa dipulihkan. Sedangkan dengan copy, sumber tetap utuh bila terjadi kegagalan, jadi jaminan strong tetap terjaga. Dengan noexcept pada move, compiler tahu operasi pemindahan itu tak mungkin gagal, dan jalur cepat aman dipakai. Tipe standar seperti std::string sudah menandai move-nya noexcept; pastikan class Anda mengikuti, terutama yang akan disimpan di container.
noexcept melengkapi perangkat exception kita: jaminan bisa dinyatakan, diperiksa compile time, dan dimanfaatkan pustaka. Namun ada keluarga kegagalan yang lebih cocok dilaporkan tanpa exception sama sekali — kegagalan yang memang diharapkan, seperti berkas yang belum ada. Chapter berikutnya memperkenalkan wakilnya: std::error_code.
Referensi: cppreference — noexcept specifier , cppreference — noexcept operator , cppreference — is_nothrow_move_constructible .