Fırlatmaktan Yönetmeye
Bir önceki sayfada, bir şeyler ters gittiğinde nasıl istisna throw edileceğini (fırlatılacağını) öğrendiniz. Fırlatmak hikâyenin yalnızca yarısıdır; hiç yakalanmayan bir istisna std::terminate çağırır ve programınızı çökertir. try/catch deyimi, fırlatılanı yönetmenin ve çalışmaya devam etmenin yoludur.
Yapısı basittir: riskli kodu bir try bloğunun içine koyun ve onu belirli hata türlerine tepki veren bir veya daha fazla catch bloğuyla takip ettirin. try bloğu sorunsuz çalışırsa her catch atlanır. Bir şey fırlatıldığı anda, kontrol doğrudan ilk eşleşen catch'e atlar.
"after" ifadesinin asla yazdırılmadığına dikkat edin. throw tetiklenir tetiklenmez, try bloğunun geri kalanı terk edilir ve yürütme eşleşen catch içinde devam eder. catch bittikten sonra program altından normal şekilde devam eder.
const Referansla Yakalama
C++ hata yönetimindeki en önemli alışkanlık: istisnaları değerle değil, const referansla yakalayın.
Değerle yakalamak istisnayı kopyalar ve daha kötüsü onu dilimler (slicing). Standart istisnalar bir hiyerarşi oluşturur (runtime_error ve logic_error ikisi de std::exception'dan türer), bu yüzden türetilmiş bir istisnayı temel bir değer olarak yakalamak türetilmiş kısmı keser. Referansla yakalamak nesneyi bütün ve polimorfik tutar:
Burada bir out_of_range fırlatıyoruz ama onu const exception& olarak yakalıyoruz. out_of_range, exception'dan türediği için temel sınıf işleyicisi eşleşir ve referans sayesinde e.what() hâlâ gerçek mesajı döndürür. catch (exception e) (değerle) yazsaydınız, nesne sade bir exception'a dilimlenir ve belirli mesajı kaybedebilirdiniz.
Birden Fazla catch Bloğu
Tek bir try'ı, her biri farklı bir istisna türü için olan birden fazla catch bloğu izleyebilir. C++ bunları yukarıdan aşağıya dener ve eşleşen ilk bloğu çalıştırır; bu yüzden onları en özelden en geneline doğru sıralayın.
invalid_argument, exception'dan daha özel olduğu için önce gelmelidir. Sırayı tersine çevirip catch (const exception&)'ı üste koysaydınız, her istisnayı yutardı; altındaki invalid_argument işleyicisi asla çalışamayan ölü kod hâline gelirdi. Birçok derleyici bu konuda uyarı verir, ancak dil sizi durdurmaz.
catch (...) ve Yeniden Fırlatma
Bazen öngörmediğiniz herhangi bir şey için bir güvenlik ağı istersiniz. Genel yakalayıcı catch (...), std::exception'dan türemeyenler dâhil her istisna türüyle eşleşir (biri throw 42; veya throw "oops"; yazabilir).
İşin püf noktası, hiçbir nesne elde edememenizdir; inceleyecek bir e yoktur. Bu yüzden catch (...) en iyi son çare olarak kullanılır: bir şeyin başarısız olduğunu günlüğe kaydetmek veya temizlik yapıp yeniden fırlatmak için.
Geçerli istisnayı yeniden fırlatmak için (yerel bir temizlik veya günlükleme yaptıktan sonra onu daha dıştaki bir işleyiciye iletmek için) işlenensiz bir throw; kullanın. Bu, sade bir kopyayı yeniden fırlatacak olan throw e;'nin aksine, orijinal istisnayı (gerçek türünü ve mesajını) korur:
İçteki işleyici günlüğe kaydedip yeniden fırlatır; ardından main'deki dıştaki işleyici onunla ilgilenir. Bunun için işlenensiz throw; kullanın, asla throw e; değil.
Yığın Çözme (Stack Unwinding) ve RAII
Bir istisna bir try bloğundan dışarı yayıldığında, C++ yığın çözme (stack unwinding) gerçekleştirir: throw ile eşleşen catch arasındaki her yerel nesnenin yıkıcısı, oluşturulmanın tersi sırayla çağrılır. İstisnaları güvenli kılan budur: yığın nesnelerinin tuttuğu kaynaklar otomatik olarak serbest bırakılır.
Tam olarak bu yüzden kaynakları manuel new/delete yerine RAII türlerinde (örneğin std::vector, std::string ve akıllı işaretçiler) tutmalısınız. Bir istisna manuel bir tahsisi kestiğinde ne olduğuna bakın:
void leaky() {
int* buffer = new int[1000];
mightThrow(); // bu fırlatırsa, sonraki satır asla çalışmaz...
delete[] buffer; // ...ve buffer sızar
}
throw, delete[]'in üzerinden atladığı için bellek kaybedilir. Akıllı bir işaretçi bunu bedavaya düzeltir; yıkıcısı yığın çözme sırasında çalışır:
void safe() {
auto buffer = std::make_unique<int[]>(1000);
mightThrow(); // bu fırlatsa bile, buffer'ın yıkıcısı belleği yine de serbest bırakır
} // manuel delete yok, sızıntı yok, istisna yolunda bile
Çıkarılacak ders: yalnızca bir şeyi delete etmek için bir istisnayı catch etmeye çalışmayın. Temizliği yıkıcılara bırakın ve catch'i nasıl toparlanacağına karar vermek için saklayın.
Yaygın Hatalar ve Tuzaklar
Birkaç tuzak tekrar tekrar karşımıza çıkar:
İstisnaları normal kontrol akışı için kullanmayın. Fırlatmak ve yığın çözmek, basit bir if'ten çok daha yavaştır. İstisnaları gerçekten istisnai hata durumları için saklayın, "kullanıcı boş bir dize yazdı" için değil.
Boş bir catch bloğu hataları gizler. Bir hatayı susturmak için catch (...) {} yazmak, başarısızlıkların iz bırakmadan kaybolması anlamına gelir. En azından sorunu günlüğe kaydedin; genellikle onu yeniden fırlatmalı veya düzgün şekilde ele almalısınız.
Fırlatan bir yıkıcı tehlikelidir. Bir yıkıcı, yığın çözme sırasında (başka bir istisna zaten devredeyken) fırlatırsa, program std::terminate çağırır. Modern C++'ta yıkıcılar örtük olarak noexcept'tir; asla bir istisnanın birinden kaçmasına izin vermeyin.
catch yalnızca try'ın kapsadığını görür. try'a girmeden önce veya içindeki çağrı yolunda olmayan farklı bir fonksiyonda fırlatılan bir istisna burada yakalanmaz. catch yalnızca kendi try bloğunun içinde çalışan kodu (doğrudan veya çağırdığı fonksiyonlarda) korur.
Sıradaki: Tanımsız Davranış
İstisnalar, C++'ın bir şeylerin ters gittiğini size söylemenin tanımlı yoludur: fırlatırsınız, yakalarsınız, davranış öngörülebilirdir. Ama C++'ın dilin hiçbir söz vermediği daha karanlık bir köşesi de vardır: sarkan bir işaretçinin başvurusunu kaldırma, bir dizinin sonunu aşarak okuma, işaretli tam sayı taşması. Sonraki sayfa tanımsız davranışı ele alır: onu neyin tetiklediğini, felaket şekilde çalışmayı durdurana kadar neden "çalışıyor" gibi görünebileceğini ve onu kodunuzdan nasıl uzak tutacağınızı.
Sıkça Sorulan Sorular
C++'ta try-catch nasıl çalışır?
İstisna fırlatabilecek kodu bir try { } bloğunun içine koyarsınız. Bir istisna fırlatılırsa, program try bloğunun geri kalanını çalıştırmayı durdurur ve hatayı ele aldığınız ilk eşleşen catch bloğuna atlar. Hiçbir şey fırlatılmazsa, catch blokları tamamen atlanır.
C++'ta istisnaları neden const referansla yakalamalısınız?
Referansla yakalamak (catch (const std::exception& e)) istisna nesnesini kopyalamaktan kaçınır ve en önemlisi polimorfizmi korur; böylece temel türü olarak yakalanan türetilmiş bir istisna hâlâ doğru what() çağrısını yapar. Değerle yakalamak (catch (std::exception e)) türetilmiş kısmı kesip atar ve bilgi kaybına yol açabilir.
C++'ta herhangi bir istisna nasıl yakalanır?
catch (...) kullanın; üç nokta, türü ne olursa olsun her istisnayı yakalar. Son çare olarak kullanışlı bir işleyicidir, ancak inceleyebileceğiniz bir nesne elde edemediğiniz için onu belirli catch bloklarınızdan sonra koyun ve esas olarak günlüğe kaydetmek veya yeniden fırlatmak için kullanın.