← Kursa Dön
📄 Text · 15 min

Exception Handling

Programlar her zaman planlandığı gibi çalışmaz. Dosya bulunamaz, bellek yetmez, kullanıcı saçma bir değer girer. Peki bu durumlarda programın çökmesini mi izleyelim? Tabii ki hayır. C++'ta exception handling (istisna yönetimi) mekanizması tam da bu iş için var.

Exception handling'i şöyle düşün: bir fabrikada hatalı ürün üretildiğinde, üretim hattının tamamı durmaz. Hatalı ürün bir kenara ayrılır, incelenir ve gerekli müdahale yapılır. try-catch mekanizması da programındaki "kalite kontrol hattı" gibi çalışır.


try-catch Blokları

Exception handling'in temeli üç anahtar kelimeden oluşur: try, catch ve throw. Mantık basit:

  1. Hata çıkabilecek kodu try bloğuna koy

  2. Hata olursa throw ile bir exception fırlat

  3. Fırlatılan exception'ı catch bloğunda yakala

#include <iostream>
#include <string>

int main() {
    try {
        int yas = -5;
        if (yas < 0) {
            throw std::string("Yas negatif olamaz!");
        }
        std::cout << "Yas: " << yas << "\n";
    }
    catch (const std::string& hata) {
        std::cout << "Hata yakalandi: " << hata << "\n";
    }

    std::cout << "Program devam ediyor.\n";
    return 0;
}

Dikkat et: throw çalıştığı anda, try bloğunun geri kalanı atlanır ve doğrudan catch bloğuna gidilir. Ama programın kendisi çökmez — catch bloğundan sonra hayat devam eder.

Birden Fazla catch Bloğu

Farklı türde hatalar farklı catch blokları ile yakalanabilir. Tıpkı bir hastanenin farklı bölümleri gibi — kırık için ortopedi, ateş için dahiliye.

#include <iostream>
#include <stdexcept>

void islemiYap(int deger) {
    if (deger == 0) {
        throw std::runtime_error("Sifira bolme hatasi!");
    }
    if (deger < 0) {
        throw std::invalid_argument("Negatif deger kabul edilmez!");
    }
    std::cout << "Sonuc: " << (100 / deger) << "\n";
}

int main() {
    try {
        islemiYap(-3);
    }
    catch (const std::invalid_argument& e) {
        std::cout << "Gecersiz arguman: " << e.what() << "\n";
    }
    catch (const std::runtime_error& e) {
        std::cout << "Calisma zamani hatasi: " << e.what() << "\n";
    }
    catch (...) {
        std::cout << "Bilinmeyen bir hata olustu!\n";
    }

    return 0;
}

Son catch (...) bloğu bir "joker" gibidir — önceki catch'lerin hiçbirine uymayan her şeyi yakalar. Güvenlik ağı olarak düşün.

💡 catch blokları sıralıdır. Derleyici yukarıdan aşağıya kontrol eder. Bu yüzden en spesifik catch'i en üste, en genel olanı (catch (...)) en alta koy.


throw ile Exception Fırlatma

throw anahtar kelimesi herhangi bir türde değer fırlatabilir — int, string, hatta kendi yazdığın bir sınıf bile olabilir. Ama pratikte genellikle std::exception türevleri kullanılır.

#include <iostream>
#include <stdexcept>

double bolme(double pay, double payda) {
    if (payda == 0.0) {
        throw std::runtime_error("Payda sifir olamaz!");
    }
    return pay / payda;
}

int main() {
    try {
        double sonuc = bolme(10.0, 0.0);
        std::cout << "Sonuc: " << sonuc << "\n";
    }
    catch (const std::runtime_error& e) {
        std::cout << "Hata: " << e.what() << "\n";
    }

    return 0;
}

throw bir fonksiyonun derinlerinden bile çağrılabilir. Exception, çağrı yığınında (call stack) geriye doğru yayılır — ta ki bir catch bloğu onu yakalayana kadar. Hiçbir catch yoksa program çöker (std::terminate çağrılır).

Exception Yeniden Fırlatma (Rethrow)

Bazen bir exception'ı yakalayıp, biraz işlem yaptıktan sonra tekrar fırlatmak istersin. Bunun için throw; (parametresiz) kullanılır.

#include <iostream>
#include <stdexcept>

void ortaKatman() {
    try {
        throw std::runtime_error("Orijinal hata");
    }
    catch (const std::exception& e) {
        std::cout << "Loglandi: " << e.what() << "\n";
        throw;  // ayni exception'i tekrar firlat
    }
}

int main() {
    try {
        ortaKatman();
    }
    catch (const std::exception& e) {
        std::cout << "Main yakaladi: " << e.what() << "\n";
    }
    return 0;
}

⚠️ `throw;` ile `throw e;` aynı şey değildir! throw e; exception'ı kopyalar ve tür bilgisini kaybedebilir (object slicing). throw; orijinal exception'ı olduğu gibi yeniden fırlatır. Her zaman throw; tercih et.


std::exception Hiyerarşisi

C++ standart kütüphanesi, exception'lar için hazır bir sınıf hiyerarşisi sunar. Tepede std::exception sınıfı bulunur. Tüm standart exception'lar bundan türer.

std::exception
├── std::logic_error
│   ├── std::invalid_argument
│   ├── std::domain_error
│   ├── std::length_error
│   └── std::out_of_range
├── std::runtime_error
│   ├── std::overflow_error
│   ├── std::underflow_error
│   └── std::range_error
└── std::bad_alloc

logic_error ailesi: Programcının hatasıdır. Kodda bir mantık bozukluğu var demektir. Örneğin geçersiz bir argüman geçmek.

runtime_error ailesi: Çalışma zamanında oluşan, önceden tahmin edilemeyecek hatalardır. Örneğin bir dosyanın bozuk olması.

bad_alloc: new ile bellek ayrılamadığında fırlatılır.

Her exception sınıfının what() metodu vardır — bu metot hata mesajını const char* olarak döndürür.

#include <iostream>
#include <vector>
#include <stdexcept>

int main() {
    try {
        std::vector<int> v = {1, 2, 3};
        std::cout << v.at(10) << "\n";  // out_of_range firlатır
    }
    catch (const std::out_of_range& e) {
        std::cout << "Sinir disi erisim: " << e.what() << "\n";
    }
    catch (const std::exception& e) {
        std::cout << "Genel hata: " << e.what() << "\n";
    }

    return 0;
}

v.at(10) çağrısı, vektörde 10. indeks olmadığı için std::out_of_range fırlatır. v[10] kullansaydık exception fırlatılmaz, tanımsız davranış (undefined behavior) olurdu. Güvenlik istiyorsan at() kullan.


Kendi Exception Sınıfını Yazma

Standart exception sınıfları çoğu duruma yeter. Ama bazen projeye özel hata türleri tanımlamak istersin. Bunun için std::exception'dan (veya türevlerinden) kalıtım alırsın.

#include <iostream>
#include <stdexcept>
#include <string>

class VeriTabaniHatasi : public std::runtime_error {
public:
    VeriTabaniHatasi(const std::string& mesaj, int hataKodu)
        : std::runtime_error(mesaj), hataKodu_(hataKodu) {}

    int hataKodu() const { return hataKodu_; }

private:
    int hataKodu_;
};

void veritabaninaBaglan() {
    // Baglanti basarisiz oldu diyelim
    throw VeriTabaniHatasi("Baglanti reddedildi", 5001);
}

int main() {
    try {
        veritabaninaBaglan();
    }
    catch (const VeriTabaniHatasi& e) {
        std::cout << "DB Hatasi: " << e.what() << "\n";
        std::cout << "Hata kodu: " << e.hataKodu() << "\n";
    }
    catch (const std::exception& e) {
        std::cout << "Genel hata: " << e.what() << "\n";
    }

    return 0;
}

Kendi exception sınıfın ekstra bilgi taşıyabilir — hata kodları, dosya adları, satır numaraları gibi. Bu, hata ayıklama sırasında inanılmaz faydalıdır.

Bir diğer yaygın kalıp da farklı hata kategorileri için bir exception hiyerarşisi oluşturmaktır:

#include <iostream>
#include <stdexcept>
#include <string>

// Temel uygulama hatasi
class AppHatasi : public std::runtime_error {
public:
    using std::runtime_error::runtime_error;
};

// Ag ile ilgili hatalar
class AgHatasi : public AppHatasi {
public:
    using AppHatasi::AppHatasi;
};

// Kimlik dogrulama hatalari
class YetkilendirmeHatasi : public AppHatasi {
public:
    using AppHatasi::AppHatasi;
};

void apiIstegi() {
    throw YetkilendirmeHatasi("Token suresi dolmus");
}

int main() {
    try {
        apiIstegi();
    }
    catch (const YetkilendirmeHatasi& e) {
        std::cout << "Yetkilendirme: " << e.what() << "\n";
    }
    catch (const AppHatasi& e) {
        std::cout << "Uygulama hatasi: " << e.what() << "\n";
    }

    return 0;
}

💡 using ile constructor'ı miras almak (using AppHatasi::AppHatasi;), C++11 sonrası çok kullanışlı bir kalıptır. Base sınıfın constructor'larını tek satırda kendi sınıfına taşır.


noexcept Keyword

Bir fonksiyonun asla exception fırlatmayacağını garanti etmek istiyorsan noexcept kullanırsın. Bu, derleyiciye bir söz vermektir: "Bu fonksiyon hata fırlatmaz."

#include <iostream>

int topla(int a, int b) noexcept {
    return a + b;
}

void tehlikeliFonksiyon() noexcept {
    throw std::runtime_error("Ups!");  // YANLIS! terminate cagirilir
}

int main() {
    std::cout << topla(3, 5) << "\n";

    // tehlikeliFonksiyon();  // Bu cagirilirsa program coker!
    return 0;
}

noexcept olan bir fonksiyon exception fırlatırsa, program doğrudan std::terminate() ile sonlanır — catch falan çalışmaz. Bu yüzden noexcept kullanırken emin ol.

noexcept Ne Zaman Kullanılmalı?

  • Move constructor ve move assignment operator: STL container'ları (vector gibi) elemanları taşırken, move işleminin noexcept olup olmadığını kontrol eder. noexcept değilse kopyalama yapar — bu da performans kaybıdır.

  • Destructor'lar: Zaten varsayılan olarak noexcept'tir. Destructor'dan exception fırlatmak neredeyse her zaman yanlıştır.

  • Swap fonksiyonları: Exception fırlatmaması beklenir.

  • Basit getter/setter'lar: Sadece bir değer döndüren fonksiyonlar.

#include <iostream>
#include <utility>

class Tampon {
    int* veri_;
    size_t boyut_;
public:
    Tampon(size_t n) : veri_(new int[n]), boyut_(n) {}

    ~Tampon() noexcept {
        delete[] veri_;
    }

    // Move constructor — noexcept olmasi onemli!
    Tampon(Tampon&& diger) noexcept
        : veri_(diger.veri_), boyut_(diger.boyut_) {
        diger.veri_ = nullptr;
        diger.boyut_ = 0;
    }

    // Move assignment — noexcept olmasi onemli!
    Tampon& operator=(Tampon&& diger) noexcept {
        if (this != &diger) {
            delete[] veri_;
            veri_ = diger.veri_;
            boyut_ = diger.boyut_;
            diger.veri_ = nullptr;
            diger.boyut_ = 0;
        }
        return *this;
    }

    size_t boyut() const noexcept { return boyut_; }
};

⚠️ Destructor'dan asla exception fırlatma. Stack unwinding sırasında bir destructor exception fırlatırsa, zaten aktif bir exception varken ikinci bir exception oluşur ve program std::terminate() ile çöker.

noexcept Operatörü

noexcept aynı zamanda bir operatör olarak da kullanılabilir — bir ifadenin exception fırlatıp fırlatmayacağını derleme zamanında kontrol eder.

#include <iostream>

void guvenli() noexcept {}
void tehlikeli() {}

int main() {
    std::cout << std::boolalpha;
    std::cout << "guvenli noexcept mi? " << noexcept(guvenli()) << "\n";    // true
    std::cout << "tehlikeli noexcept mi? " << noexcept(tehlikeli()) << "\n";  // false
    return 0;
}

Exception Safety Seviyeleri

Exception handling sadece "hatayı yakala" demek değil. Daha derin bir soru var: exception fırlatıldığında programın durumu ne olur? Veriler tutarlı mı kalır? Bellek sızıyor mu?

Bu sorulara cevap veren üç "güvenlik seviyesi" tanımlanmıştır:

1. Nothrow Guarantee (Fırlatmama Garantisi)

Fonksiyon asla exception fırlatmaz. noexcept ile işaretlenir. En güçlü garantidir.

void swap(int& a, int& b) noexcept {
    int temp = a;
    a = b;
    b = temp;
}

2. Strong Guarantee (Güçlü Garanti)

Exception fırlatılırsa, programın durumu fonksiyon çağrılmadan önceki haline döner. "Ya hep ya hiç" prensibi. Sanki fonksiyon hiç çağrılmamış gibi.

#include <vector>
#include <stdexcept>
#include <algorithm>

class Veritabani {
    std::vector<std::string> kayitlar_;
public:
    void kayitEkle(const std::string& kayit) {
        // Once yedeği al
        auto yedek = kayitlar_;

        // Islemleri yedek uzerinde yap
        yedek.push_back(kayit);

        if (kayit.empty()) {
            throw std::invalid_argument("Bos kayit eklenemez");
            // yedek yok olur, orijinal veritabani etkilenmez
        }

        // Basariliysa, swap ile degistir (noexcept)
        std::swap(kayitlar_, yedek);
    }
};

Bu tekniğe "copy-and-swap" denir. Önce bir kopya üzerinde çalışırsın, her şey başarılıysa swap ile asıl veriyi değiştirirsin.

3. Basic Guarantee (Temel Garanti)

Exception fırlatılırsa, program tutarlı bir durumda kalır ve kaynak sızıntısı olmaz. Ama önceki duruma dönme garantisi yoktur — veriler değişmiş olabilir.

#include <vector>

class Liste {
    std::vector<int> elemanlar_;
public:
    void topluEkle(const std::vector<int>& yeniler) {
        for (const auto& eleman : yeniler) {
            elemanlar_.push_back(eleman);  // ortada exception olursa
            // bazi elemanlar eklenmis, bazilari eklenmemis olabilir
            // ama bellek sizintisi yok, nesne tutarli durumda
        }
    }
};

Hangi Seviyeyi Hedeflemeli?

  • Nothrow: Destructor'lar, move işlemleri, swap. Mutlaka.

  • Strong: Veritabanı işlemleri, dosya yazma gibi kritik operasyonlar. Mümkünse.

  • Basic: Minimum kabul edilebilir seviye. Her fonksiyon en azından bunu sağlamalı.

Hiçbir garanti vermeyen kod yazmak tehlikelidir — bellek sızar, veriler bozulur, programın durumu belirsiz hale gelir. RAII (Resource Acquisition Is Initialization) kullanarak basic guarantee'yi neredeyse otomatik olarak sağlayabilirsin. Bunu ilerideki derslerde detaylıca göreceğiz.


Exception Handling Best Practices

Exception handling'i doğru kullanmak, kötü kullanmaktan çok daha önemlidir. İşte birkaç altın kural:

1. Exception'ları kontrol akışı için kullanma. Exception'lar istisnai durumlar içindir. Normal program akışını yönetmek için if-else kullan.

// YANLIS — exception'i if-else yerine kullanmak
try {
    auto deger = harita.at(anahtar);
} catch (...) {
    // anahtar yok
}

// DOGRU
if (harita.count(anahtar)) {
    auto deger = harita.at(anahtar);
}

2. Exception'ları const referans ile yakala. Kopyalama maliyetinden kaçın ve object slicing'i önle.

// DOGRU
catch (const std::exception& e) { ... }

// YANLIS — gereksiz kopyalama, slicing riski
catch (std::exception e) { ... }

3. Çok genel catch yazma — ama en dışta bir tane bulundur.

int main() {
    try {
        uygulamayiCalistir();
    }
    catch (const std::exception& e) {
        std::cerr << "Beklenmeyen hata: " << e.what() << "\n";
        return 1;
    }
    catch (...) {
        std::cerr << "Bilinmeyen hata!\n";
        return 2;
    }
    return 0;
}

4. Constructor'da exception fırlatmak güvenlidir — nesne tam oluşmadığı için destructor çağrılmaz, ama üye değişkenlerin destructor'ları çağrılır (RAII sayesinde).

5. Destructor'dan exception fırlatma — stack unwinding sırasında std::terminate() çağrılır.


Özet

  • try-catch-throw üçlüsü, C++'ın hata yönetim mekanizmasıdır. try bloğunda riskli kod çalışır, hata olursa throw ile fırlatılır, catch ile yakalanır.

  • std::exception hiyerarşisi, logic_error ve runtime_error olmak üzere iki ana daldan oluşur. Kendi exception sınıflarını bunlardan türetebilirsin.

  • noexcept, bir fonksiyonun exception fırlatmayacağını garanti eder. Move constructor, destructor ve swap için kritik öneme sahiptir.

  • Exception safety seviyeleri üçtür: nothrow (asla fırlatmaz), strong (ya hep ya hiç), basic (tutarlı durum garantisi). Her fonksiyon en az basic guarantee sağlamalıdır.

  • Exception'ları her zaman const referans ile yakala, kontrol akışı için kullanma ve destructor'lardan fırlatma.

  • Rethrow yaparken throw; kullan, throw e; değil — orijinal exception türünü korumak için.