Standar C11 menambahkan kata kunci baru yang menuliskan kontrak kode secara eksplisit, yaitu pemilihan ekspresi menurut tipe, pemeriksaan asumsi saat kompilasi, penanda fungsi yang tidak kembali, dan janji kepemilikan pointer. Topik ini membahas _Generic, _Static_assert, inline, restrict, dan _Noreturn, masing-masing dengan contoh yang dapat dijalankan. Seluruhnya aktif pada gcc dengan pengaturan bawaan, sehingga contohnya dapat langsung dikompilasi tanpa opsi tambahan.
_Generic: pemilihan menurut tipe#
Kata kunci _Generic memilih salah satu ekspresi berdasarkan tipe argumennya, bukan berdasarkan nilainya. Bentuknya menyerupai switch, tetapi kasusnya berupa tipe, dan hanya cabang yang cocok yang dikompilasi ke dalam program. Karena penilaiannya terjadi saat kompilasi, _Generic biasanya dibungkus makro agar terpakai seperti fungsi biasa.
#include <stdio.h>
#define NAMA_TIPE(x) _Generic((x), \
int: "int", \
double: "double", \
char *: "char *", \
default: "tipe lain")
int main(void) {
int umur = 17;
double suhu = 36.5;
char *sapaan = "Salam";
printf("umur : %s\n", NAMA_TIPE(umur));
printf("suhu : %s\n", NAMA_TIPE(suhu));
printf("sapaan : %s\n", NAMA_TIPE(sapaan));
printf("3.0f : %s\n", NAMA_TIPE(3.0f));
return 0;
}umur : int
suhu : double
sapaan : char *
3.0f : tipe lainKasus default menampung tipe yang tidak disebutkan, sehingga literal 3.0f bertipe float jatuh ke situ. Hanya cabang yang tipenya cocok yang dieksekusi ketika program berjalan, sedangkan cabang lainnya tidak pernah dievaluasi sama sekali. Kegunaan utama _Generic adalah membuat satu makro yang berperilaku sesuai tipe argumennya, misalnya memilih fungsi pembulatan atau penentu format yang tepat. Dengan begitu pengguna tidak perlu memanggil nama fungsi yang berbeda untuk tiap tipe. Pustaka math.h memakai mekanisme serupa pada keluarga fungsi matematika generiknya.
Setiap tipe hanya boleh muncul satu kali pada satu _Generic. Apabila lebih dari satu kasus dapat cocok dengan argumen yang sama, kompilasi ditolak. Kasusnya ditulis dengan tanda titik dua mengikuti pola tipe: ekspresi, dan cabang default menampung sisanya. Karena pemilihannya berlangsung saat kompilasi, mekanisme ini tidak menambah percabangan apa pun pada program yang berjalan.
_Static_assert: pemeriksaan pada waktu kompilasi#
_Static_assert memeriksa sebuah kondisi konstanta saat kompilasi, dan pemeriksaan yang gagal menghentikan kompilasi dengan pesan yang ditulis sendiri oleh pemrogram. Penempatannya bisa di luar fungsi untuk asumsi platform atau di dalam fungsi untuk invarian lokal. Program berikut menjaga dua asumsi yang dipakai kodenya.
#include <stdio.h>
#include <limits.h>
_Static_assert(sizeof(int) >= 4, "Program menuntut int minimal 4 byte");
int main(void) {
_Static_assert(CHAR_BIT == 8, "Asumsi byte 8 bit dilanggar");
printf("Semua pemeriksaan waktu kompilasi lolos\n");
printf("Ukuran int: %zu byte\n", sizeof(int));
return 0;
}Semua pemeriksaan waktu kompilasi lolos
Ukuran int: 4 bytePada platform yang memenuhi asumsi tersebut, pemeriksaan ini tidak berdampak apa pun pada program yang dihasilkan. Kegunaannya justru muncul ketika kode dibawa ke platform lain, karena asumsi yang dilanggar berubah menjadi pesan error yang jelas, bukan kelainan perilaku yang sulit dilacak. Berkas berikut sengaja dibuat gagal untuk memperlihatkan bentuk pesannya.
// tidak-dijalankan
#include <stdio.h>
#define BANYAK_DATA 7
int main(void) {
_Static_assert(BANYAK_DATA % 2 == 0, "BANYAK_DATA harus genap");
printf("Banyak data: %d\n", BANYAK_DATA);
return 0;
}# mengompilasi berkas dengan pemeriksaan yang sengaja gagal
gcc gagal-assert.c -o gagal-assertgagal-assert.c: In function ‘main’:
gagal-assert.c:7:5: error: static assertion failed: "BANYAK_DATA harus genap"
7 | _Static_assert(BANYAK_DATA % 2 == 0, "BANYAK_DATA harus genap");
| ^~~~~~~~~~~~~~Pesan error itu memuat teks asertanya, sehingga pembaca kode langsung tahu aturan mana yang dilanggar tanpa membuka kode sumbernya. Pada C11, pesan asersi wajib dituliskan, dan kebiasaan yang baik adalah merumuskannya sebagai pernyataan yang dipenuhi, sebagaimana BANYAK_DATA harus genap pada contoh.
Pola lain yang lazim adalah menjaga tata letak struct yang dikirim melalui jaringan atau ditulis ke berkas, misalnya _Static_assert(sizeof(struct Paket) == 16, "Format berkas bergantung pada ukuran ini");. Perubahan anggota struct yang diam-diam menggeser ukurannya langsung terdeteksi pada waktu kompilasi, sebelum format berkas rusak di tempat pemakaiannya. Asersi semacam ini tidak menambah satu byte pun pada program akhir karena seluruhnya hilang setelah kompilasi.
inline dan restrict#
Kata kunci inline merupakan saran kepada kompiler agar pemanggilan fungsi digantikan oleh tubuh fungsinya, sehingga biaya pemanggilan hilang. Karena bersifat saran, kompiler boleh mengabaikannya, dan kompiler modern justru lebih menentukan berdasarkan hasil analisis kecepatan serta ukuran kodenya. Bentuk yang lazim ditulis pada header memadukan inline dengan static.
// tidak-dijalankan
static inline int kuadrat(int x) {
return x * x;
}Kombinasi static inline membuat setiap berkas yang memuat header memiliki salinan fungsi itu sendiri, sehingga tidak ada definisi ganda pada tahap pengaitan. Kata kunci berikutnya, restrict, menyatakan janji bahwa objek yang diakses melalui sebuah pointer tidak diakses melalui pointer lain dalam lingkup yang sama. Janji ini membuka peluang optimasi yang semula tidak tersedia bagi kompiler, dan memcpy pada pustaka standar memakai kata kunci ini pada parameternya.
#include <stdio.h>
void salin(int *restrict tujuan, const int *restrict sumber, int banyak) {
for (int i = 0; i < banyak; i++) {
tujuan[i] = sumber[i];
}
}
int main(void) {
int sumber[5] = {10, 20, 30, 40, 50};
int tujuan[5];
salin(tujuan, sumber, 5);
for (int i = 0; i < 5; i++) {
printf("%d ", tujuan[i]);
}
printf("\n");
return 0;
}10 20 30 40 50Fungsi salin menjanjikan bahwa tujuan dan sumber tidak saling bertumpuk, dan pemanggil pada contoh memenuhinya karena keduanya menunjuk array yang berbeda. Dengan janji itu, kompiler boleh menggeser urutan pembacaan dan penulisan secara bebas karena operasi keduanya dipastikan tidak saling mengganggu.
_Noreturn#
Penanda _Noreturn menyatakan bahwa sebuah fungsi tidak pernah kembali ke pemanggilnya, biasanya karena seluruh jalurnya berakhir dengan exit, abort, atau lompatan non-lokal. Informasi ini berguna bagi kompiler maupun pembaca kode, karena kode setelah pemanggilan tersebut dapat diperlakukan sebagai jalur yang tak terjangkau. Program berikut menandai fungsi penanganan error yang selalu mengakhiri program.
#include <stdio.h>
#include <stdlib.h>
_Noreturn void berhenti(const char *pesan) {
fprintf(stderr, "FATAL: %s\n", pesan);
exit(1);
}
int proses(int nilai) {
if (nilai < 0) {
berhenti("nilai negatif tidak didukung");
}
return nilai * 2;
}
int main(void) {
printf("proses(21) = %d\n", proses(21));
return 0;
}proses(21) = 42Penanda ini paling sering dipakai pada tiga jenis fungsi, yaitu pembungkus penanganan error yang memanggil exit, fungsi yang langsung memanggil abort, dan pembungkus longjmp yang melompat ke tempat lain. Fungsi berhenti pada program di atas termasuk jenis pertama, dan pemanggilannya di dalam proses membuat cabang nilai negatif tidak pernah sampai ke pernyataan kembali. Kompiler yang mengetahui kontrak ini dapat membuang pemeriksaan kembali yang tidak diperlukan, sedangkan pembaca kode langsung memahami bahwa pemanggilan tersebut adalah jalan buntu yang disengaja.
Kesalahan umum#
- Mengira
_Genericmenilai nilai. Kasus pada_Genericwajib berupa tipe, sehingga penulisan_Generic(x, 1: "satu", default: "bukan")ditolak kompiler karena1bukan tipe. Nilai yang ingin dibedakan tetap tugasswitchatauif. - Melanggar janji
restrict. Mengirim dua pointer yang menunjuk memori bertumpuk, misalnyasalin(buf, buf + 2, 3), membatalkan janji tersebut dan menghasilkan perilaku tak terdefinisi yang wujudnya bergantung tingkat optimasi. Pastikan rentang memori kedua pointer memang terpisah sebelum menandai parameter denganrestrict. - Menandai fungsi
_Noreturnyang sebenarnya bisa kembali. Apabila ada jalur eksekusi yang kembali dari fungsi tersebut, jalur itu berupa perilaku tak terdefinisi. Pastikan setiap jalur diakhiriexit,abort, atau lompatan sebelum memberi penandanya. - Mengira
inlinemenjamin penempelan tubuh fungsi. Kata kunci ini hanya saran, dan kompiler tetap boleh mempertahankan pemanggilan karena pertimbangan ukuran maupun kecepatannya. Ukur keputusan optimasi dengan hasil kompilasi, bukan dengan keberadaan kata kuncinya, dan bentukstatic inlinepada header aman dipakai karena setiap berkas memegang salinannya sendiri.
Ringkasan#
_Genericmemilih ekspresi berdasarkan tipe argumen pada waktu kompilasi, sehingga satu makro dapat melayani beberapa tipe._Static_assertmengubah asumsi yang dilanggar menjadi pesan error saat kompilasi, dengan pesan yang wajib dituliskan pada C11.inlinemerupakan saran penggantian pemanggilan dengan tubuh fungsi, sedangkanrestrictmenjanjikan tidak adanya akses memori yang bertumpuk._Noreturnmenandai fungsi yang tidak pernah kembali, dan fungsi yang melanggar penandanya menghasilkan perilaku tak terdefinisi.
Referensi#
- Generic selection — cppreference.com
— Kaidah
_Genericbeserta aturan tipe yang cocok. - Static assertion — cppreference.com
— Sintaks dan batasan
_Static_assertpada C11. - restrict type qualifier — cppreference.com
— Makna janji
restrictdan akibat pelanggarannya. - _Noreturn function specifier — cppreference.com — Kontrak fungsi yang tidak kembali beserta akibat pelanggarannya.