Ders 12 / 14
İstisnalar
Fırlatılan bir nesnenin yakalanırken kaç kez kopyalandığının, yarıda kesilen bir yapımın hangi parçaları yıktığının ve geçici bir nesnenin isimli bir yerelden daha az kopyalandığının ölçümü.
Yayın Güncelleme
Bu derste
- Değerle yakalamanın referansla yakalamadan bir kopya fazla ürettiğini sayaçla ölçer
- Yapımı yarıda kesen bir istisnanın, zaten kurulmuş parçaları yıktığını ama nesnenin kendisini yıkmadığını gözlemler
- İsimli bir yerel yerine geçici bir nesne fırlatmanın kopya sayısını nasıl azalttığını ölçer
İçindekiler
Önceki dört ders normal akışta tamamlanan kurucu, yıkıcı ve sanal çağrıları ölçtü. Bu
ders akışın yarıda kesildiği durumu ele alıyor: bir işlev hata fırlattığında
(throw), fırlatılan nesne catch bloğuna ulaşana kadar bir ya da daha çok kez
kopyalanabiliyor; kesilme bir kurucunun ortasında olduysa, kurulmuş parçalar yıkılıyor
ama nesnenin kendisi hiç yıkılmıyor. Ölçü sorusu: bir ifade kaç kez, hangi nesnenin
kopyasıyla çalışıyor?
Değerle Yakalama, Referansla Yakalamadan Bir Kopya Fazla Üretiyor
HataNesnesi, kopya kurucusunda bir sayaç artırıyor. Aynı isimli yerel nesneyi
fırlatıp bir kez değerle, bir kez referansla yakaladığımızda, sayaç kaç kopya
gösteriyor?
// istisna_kopya_sayaci.cpp — deger ile yakalama referansla yakalamadan bir kopya fazla uretiyor
#include <iostream>
class HataNesnesi {
public:
HataNesnesi() = default;
HataNesnesi(const HataNesnesi&) { ++kopya_sayaci; }
static int kopya_sayaci;
};
int HataNesnesi::kopya_sayaci = 0;
void firlat() {
HataNesnesi h;
throw h;
}
int main() {
try {
firlat();
} catch (HataNesnesi h) {
(void)h;
}
std::cout << "deger ile yakalamadan sonra kopya sayisi " << HataNesnesi::kopya_sayaci << "\n";
HataNesnesi::kopya_sayaci = 0;
try {
firlat();
} catch (const HataNesnesi& h) {
(void)h;
}
std::cout << "referansla yakalamadan sonra kopya sayisi " << HataNesnesi::kopya_sayaci << "\n";
return 0;
}
deger ile yakalamadan sonra kopya sayisi 2 referansla yakalamadan sonra kopya sayisi 1
CB13. throw h; isimli bir yereli fırlatıyor — istisna depolamasına bir kopya
çıkarıyor (birinci). catch (HataNesnesi h) bunu değerle yakalayıp ikinci bir
kopya çıkarıyor: toplam 2. catch (const HataNesnesi& h) depolanmış nesneye
doğrudan bağlanıyor, ek kopya çıkarmıyor: toplam 1. Sınıf kopya: değerle
yakalama, referansla yakalamanın işine bir kopya daha ekliyor.
Yapımı Yarıda Kesen İstisna: Kurulan Parçalar Yıkılıyor, Nesnenin Kendisi Yıkılmıyor
Bilesik’in kurucusu iki AltParca üyesini kurduktan sonra bir istisna fırlatıyor.
Zaten kurulmuş üyeler yıkılıyor mu? Bilesik’in kendi yıkıcısı hiç çalışıyor mu?
// istisna_yarida_kesilen_yapim.cpp — kurulan parcalar yikiliyor, nesnenin kendisi hic yikilmiyor
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
class AltParca {
public:
explicit AltParca(std::string ad) : ad_(std::move(ad)) {
std::cout << ad_ << " kuruldu\n";
}
~AltParca() {
std::cout << ad_ << " yikildi\n";
}
private:
std::string ad_;
};
class Bilesik {
public:
Bilesik() : birinci_("birinci"), ikinci_("ikinci") {
std::cout << "Bilesik govdesi calisiyor, istisna firlatiliyor\n";
throw std::runtime_error("yapim hatasi");
}
~Bilesik() {
std::cout << "Bilesik yikici calisti\n";
}
private:
AltParca birinci_;
AltParca ikinci_;
};
int main() {
try {
Bilesik b;
} catch (const std::exception& e) {
std::cout << "yakalanan hata: " << e.what() << "\n";
}
return 0;
}
birinci kuruldu ikinci kuruldu Bilesik govdesi calisiyor, istisna firlatiliyor ikinci yikildi birinci yikildi yakalanan hata: yapim hatasi
CB14. birinci_ ve ikinci_ üye ilkleme listesinde kuruluyor. Gövdede fırlatılan
istisna kurucuyu yarıda kesiyor: Bilesik hiçbir zaman tamamlanmadı. Yığıt
çözülürken o ana kadar kurulmuş üyeler ters sırayla yıkılıyor — önce ikinci_, sonra
birinci_ — ama “Bilesik yikici calisti” hiç basılmadı: yıkıcı ancak kurucu
tamamlanmışsa çağrılıyor. Sınıf kopya burada doğrudan görünmüyor, ama
catch (const std::exception& e) referansla yakalayıp ek kopya çıkarmıyor.
Geçici Bir Nesne, İsimli Bir Yerelden Daha Az Kopyalanıyor
CB13’te isimli bir yerel fırlatılıp değerle yakalanınca kopya sayısı 2 çıktı.
Doğrudan bir geçici (throw HataNesnesi();) fırlatılıp yine değerle yakalanırsa bu
sayı değişiyor mu?
// istisna_gecici_kopya.cpp — gecici nesne firlatmak isimli yerelden daha az kopyaliyor
#include <iostream>
class HataNesnesi {
public:
HataNesnesi() = default;
HataNesnesi(const HataNesnesi&) { ++kopya_sayaci; }
static int kopya_sayaci;
};
int HataNesnesi::kopya_sayaci = 0;
void firlat_gecici() {
throw HataNesnesi();
}
int main() {
try {
firlat_gecici();
} catch (HataNesnesi h) {
(void)h;
}
std::cout << "gecici nesne firlatilinca (deger ile yakalama) kopya sayisi "
<< HataNesnesi::kopya_sayaci << "\n";
return 0;
}
gecici nesne firlatilinca (deger ile yakalama) kopya sayisi 1
CB15. firlat_gecici isimli bir yerel kurmuyor, doğrudan bir geçici (prvalue)
kurup fırlatıyor; C++17’de bir geçiciden (prvalue) ilklenen istisna nesnesi
kopya kurucu çağrılmadan doğrudan istisna depolamasında kurulur, birinci kopya
düşüyor. Kalan tek kopya değerle yakalamanınki: toplam 1, CB13’teki 2’den bir
eksik. Sınıf kopya: fırlatılan ifadenin isimli yerel mi geçici mi olduğu, kopya
sayısını değiştiriyor.
Yığıt Çözülmesi Çok Çerçeveli: Her Katman Kendi Yerellerini Yıkıyor
CB14 tek bir kurucunun içinde yarıda kalan yapımı ölçtü. İstisna birkaç iç içe işlev çağrısının en derininde fırlatılırsa, ara çerçevelerdeki yerel nesneler de aynı yığıt-çözülme mekanizmasıyla yıkılıyor mu, ve hangi sırayla?
// istisna_yigit_cozulmesi.cpp — cok cerceveli yigit cozulmesinde her cerceve kendi yerellerini ters sirayla yikiyor
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
class Izleyici {
public:
explicit Izleyici(std::string ad) : ad_(std::move(ad)) {
std::cout << ad_ << " kuruldu\n";
}
~Izleyici() {
std::cout << ad_ << " yikildi (yigit cozulurken)\n";
}
private:
std::string ad_;
};
void ucuncu_seviye() {
Izleyici i3("ucuncu seviye");
throw std::runtime_error("derinlerde hata");
}
void ikinci_seviye() {
Izleyici i2("ikinci seviye");
ucuncu_seviye();
}
void birinci_seviye() {
Izleyici i1("birinci seviye");
ikinci_seviye();
}
int main() {
try {
birinci_seviye();
} catch (const std::exception& e) {
std::cout << "yakalanan hata: " << e.what() << "\n";
}
return 0;
}
birinci seviye kuruldu ikinci seviye kuruldu ucuncu seviye kuruldu ucuncu seviye yikildi (yigit cozulurken) ikinci seviye yikildi (yigit cozulurken) birinci seviye yikildi (yigit cozulurken) yakalanan hata: derinlerde hata
CB35. Üç çerçevenin yerelleri kuruluş sırasıyla basılıyor, yıkım tersi sırayla:
önce i3, sonra i2, sonra i1. Yığıt çözülmesi catch‘e ulaşana kadar geçtiği
her çerçevenin yerellerini yıkıyor — CB14’ün tek kurucudaki kuralının çağrı
yığıtının tamamına genellenmiş hâli. Sınıf kopya görünmüyor, ama düzenek aynı.
throw; Orijinal Nesneyi Koruyor, throw e; Statik Tipe Sadeliyor
Yakalanan bir istisnayı yeniden fırlatmanın iki yazımı var: parametresiz throw; ve
yakalanan değişkeni fırlatan throw e;. İkisi aynı istisnayı mı taşıyor dışarıya?
// istisna_throw_vs_throw_e.cpp — throw; orijinal tipi koruyor, throw e; parametrenin statik tipine sadeliyor
#include <iostream>
#include <stdexcept>
class TabanHata : public std::runtime_error {
public:
TabanHata() : std::runtime_error("taban hata") {}
};
class TuremisHata : public TabanHata {
public:
TuremisHata() = default;
};
void yeniden_firlat_orijinal() {
try {
throw TuremisHata();
} catch (TabanHata&) {
throw; // yakalanan nesneyi oldugu gibi, dinamik tipiyle yeniden firlatiyor
}
}
void yeniden_firlat_sadelenmis() {
try {
throw TuremisHata();
} catch (TabanHata& e) {
throw e; // e'nin statik tipinden (TabanHata) yeni bir nesne kuruyor
}
}
int main() {
try {
yeniden_firlat_orijinal();
} catch (TuremisHata&) {
std::cout << "throw; sonrasi TuremisHata olarak yakalandi\n";
} catch (TabanHata&) {
std::cout << "throw; sonrasi TabanHata olarak yakalandi\n";
}
try {
yeniden_firlat_sadelenmis();
} catch (TuremisHata&) {
std::cout << "throw e; sonrasi TuremisHata olarak yakalandi\n";
} catch (TabanHata&) {
std::cout << "throw e; sonrasi TabanHata olarak yakalandi\n";
}
return 0;
}
throw; sonrasi TuremisHata olarak yakalandi throw e; sonrasi TabanHata olarak yakalandi
CB36. throw; yakalanan nesneyi olduğu gibi yeniden fırlatıyor, dış catch onu
hâlâ TuremisHata olarak yakalıyor — dinamik tip korunuyor. throw e; ise
e‘nin statik tipinden (TabanHata) yeni bir nesne kuruyor; catch (TuremisHata&) artık eşleşmiyor — CB21’deki nesne dilimlemeyle aynı kök. Sınıf kopya:
throw e;, orijinalin değil e’nin statik tipinden kurulmuş bir kopyayı taşıyor.
Taban Sınıfa Değerle Yakalama, Türetilmiş İstisnayı Diliyor
TuremisHata, tip() sanal işlevini yeniden tanımlıyor. Aynı nesne bir kez değerle
bir kez referansla TabanHata olarak yakalanınca, tip() hangi sürüme ulaşıyor?
// istisna_deger_ile_yakalama_dilimleme.cpp — turetilmis istisna tabana deger ile yakalaninca diliniyor, referansla yakalaninca dilinmiyor
#include <iostream>
#include <stdexcept>
class TabanHata : public std::exception {
public:
virtual const char* tip() const { return "TabanHata"; }
};
class TuremisHata : public TabanHata {
public:
const char* tip() const override { return "TuremisHata"; }
};
void firlat_turemis() {
throw TuremisHata();
}
int main() {
try {
firlat_turemis();
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wcatch-value="
} catch (TabanHata e) { // derleyici burada bilerek deger ile yakalandigini uyariyor, olcum tam da bu davranisi gosteriyor
#pragma GCC diagnostic pop
std::cout << "deger ile yakalanan nesnenin tip() sonucu: " << e.tip() << "\n";
}
try {
firlat_turemis();
} catch (const TabanHata& e) {
std::cout << "referansla yakalanan nesnenin tip() sonucu: " << e.tip() << "\n";
}
return 0;
}
deger ile yakalanan nesnenin tip() sonucu: TabanHata referansla yakalanan nesnenin tip() sonucu: TuremisHata
CB37. catch (TabanHata e) değerle yakalıyor: nesne TabanHata tipinde yeni bir
nesneye kopyalanıyor — CB21’dekine benzer dilimleme, tip() TabanHata’ya
ulaşıyor. catch (const TabanHata& e) depolanmış TuremisHata’ya doğrudan
bağlanıyor, dilimlemiyor: TuremisHata. Bu yüzden istisna hiyerarşileri
neredeyse hep referansla yakalanır. Sınıf kopya: catch (TabanHata e)’nin
ürettiği nesne, orijinalin TabanHata parçasının bir kopyası.
Üç İstisna Güvencesi Düzeyi: Güçlü Durumu Korur, Temel Durumu Değiştirebilir
İstisna fırlatan bir işlevden sonra nesnenin durumu üç düzeyden birinde olabilir: güçlü güvence (öncesi duruma dönülür), temel güvence (geçerli ama değişmiş olabilir), fırlatmama güvencesi (sıradaki ölçüm). Aynı sınırı aşan iki ekleme işlevi — biri güçlü biri temel güvenceyle yazılmış — başarısız çağrı sonrası nasıl farklılaşıyor?
// istisna_guvence_duzeyleri.cpp — guclu guvence basarisiz cagridan sonra durumu degistirmiyor, temel guvence degistirebiliyor
#include <iostream>
#include <stdexcept>
class Sayac {
public:
void guclu_ekle(int miktar) {
int yeni_deger = deger_ + miktar; // hesap yerel degiskende, deger_ henuz degismedi
if (yeni_deger > 100) {
throw std::runtime_error("sinir asildi");
}
deger_ = yeni_deger; // yalnizca basari kesinlesince atama yapiliyor
}
void temel_ekle(int miktar) {
deger_ += miktar / 2; // once yarisi dogrudan uygulaniyor
if (deger_ + miktar / 2 > 100) {
throw std::runtime_error("sinir asildi");
}
deger_ += miktar / 2; // sonra kalan yari
}
int deger() const { return deger_; }
private:
int deger_ = 90;
};
int main() {
Sayac s1;
try {
s1.guclu_ekle(20);
} catch (const std::exception&) {
std::cout << std::boolalpha
<< "guclu_ekle basarisiz oldu, deger degisti mi: "
<< (s1.deger() != 90) << "\n";
}
Sayac s2;
try {
s2.temel_ekle(20);
} catch (const std::exception&) {
std::cout << std::boolalpha
<< "temel_ekle basarisiz oldu, deger degisti mi: "
<< (s2.deger() != 90) << "\n";
}
return 0;
}
guclu_ekle basarisiz oldu, deger degisti mi: false temel_ekle basarisiz oldu, deger degisti mi: true
CB38. guclu_ekle sınır kontrolünü yerel bir değişkende yapıp deger_’e
yalnız başarı kesinleşince atıyor; deger_ hiç dokunulmuyor: false — güçlü
güvence. temel_ekle sınırı iki adımda uygulayıp kontrolü aralarına
sıkıştırıyor; fırlatma anında deger_ zaten kısmen değişmiş: true — temel
güvence. Üçüncü düzey sıradaki ölçümde noexcept üzerinden ele alınıyor. Sınıf
kendisi: fark nesnenin kimliğinde değil, çağrının onu bıraktığı durumda.
Kopyala, Değiştir, Değiştir-Tokuş Et: Güçlü Güvenceyi Yapıdan Kazanmak
Atama işlecini elle yazmak yerine parametreyi değerle alıp bir takas işleviyle
değiştirmek de güçlü güvence sağlayan bilinen bir kalıp. Normal atamada ve öz atamada
(c = c;) doğru sonucu veriyor mu?
// istisna_kopyala_degistir_tokusla.cpp — kopyala-degistir-degistir-tokus et deseni normal atamada ve oz atamada dogru sonucu veriyor
#include <iostream>
#include <string>
#include <utility>
class Tampon {
public:
explicit Tampon(std::string veri) : veri_(std::move(veri)) {}
Tampon(const Tampon& diger) : veri_(diger.veri_) {}
friend void takas(Tampon& a, Tampon& b) noexcept {
using std::swap;
swap(a.veri_, b.veri_);
}
Tampon& operator=(Tampon diger) { // deger ile aliniyor: kopya cagrinin kendisinde olusuyor
takas(*this, diger);
return *this;
} // diger burada yikiliyor, eski veri_ onunla birlikte gidiyor
const std::string& veri() const { return veri_; }
private:
std::string veri_;
};
int main() {
Tampon a("birinci");
Tampon b("ikinci");
a = b;
std::cout << "atama sonrasi a: " << a.veri() << ", b: " << b.veri() << "\n";
Tampon c("ucuncu");
c = c;
std::cout << "oz atama sonrasi c: " << c.veri() << "\n";
return 0;
}
atama sonrasi a: ikinci, b: ikinci oz atama sonrasi c: ucuncu
CB39. operator=(Tampon diger) parametreyi değerle alıyor; diger çağrı
sırasında bağımsız bir kopya olarak kuruluyor — kopyalama, gövde çalışmadan önce,
fırlayabilecek tek yerde tamamlanıyor. Gövde yalnız noexcept takas’ı çağırıyor: bu
sıralama güçlü güvenceyi otomatik veriyor, kopyalama başarısız olursa *this hiç
değişmez. Öz atamada (c = c;) özel bir kontrole gerek kalmıyor, diger bağımsız bir
kopya olduğu için sonuç yine ucuncu. Sınıf kopya: güvence, atamadan önce ayrı
bir kopya çıkarılmasına dayanıyor.
noexcept: Fırlatmama Sözleşmesi ve Yeniden Ayırmada Taşıma Seçimi
std::vector kapasitesini aştığında yeniden ayırma yapıp eski elemanları taşıyor ya
da kopyalıyor. Standart, taşıma kurucusu noexcept değilse kopyalamayı zorunlu
kılıyor — yeniden ayırma sırasında bir taşımanın ortasında fırlayan istisna elemanları
kurtarılamaz bırakabilirdi. İki sınıf, biri noexcept taşıma kurucusuyla biri onsuz,
emplace_back ile büyüdüğünde gerçekten farklı davranıyor mu?
// istisna_noexcept_vector_secimi.cpp — noexcept isaretli tasima kurucusu vector yeniden ayirmasinda kopyanin onune geciyor
#include <iostream>
#include <vector>
class GuvenliTasima {
public:
GuvenliTasima() = default;
GuvenliTasima(const GuvenliTasima&) { ++kopya_sayaci; }
GuvenliTasima(GuvenliTasima&&) noexcept { ++tasima_sayaci; }
static int kopya_sayaci;
static int tasima_sayaci;
};
int GuvenliTasima::kopya_sayaci = 0;
int GuvenliTasima::tasima_sayaci = 0;
class GuvensizTasima {
public:
GuvensizTasima() = default;
GuvensizTasima(const GuvensizTasima&) { ++kopya_sayaci; }
GuvensizTasima(GuvensizTasima&&) { ++tasima_sayaci; } // noexcept degil
static int kopya_sayaci;
static int tasima_sayaci;
};
int GuvensizTasima::kopya_sayaci = 0;
int GuvensizTasima::tasima_sayaci = 0;
int main() {
{
std::vector<GuvenliTasima> v;
v.reserve(1);
for (int i = 0; i < 5; ++i) {
v.emplace_back();
}
}
std::cout << std::boolalpha
<< "noexcept tasima kurucusunda yeniden ayirma hic kopya kullandi mi: "
<< (GuvenliTasima::kopya_sayaci > 0) << "\n";
std::cout << "noexcept tasima kurucusunda yeniden ayirma tasima kullandi mi: "
<< (GuvenliTasima::tasima_sayaci > 0) << "\n";
{
std::vector<GuvensizTasima> v;
v.reserve(1);
for (int i = 0; i < 5; ++i) {
v.emplace_back();
}
}
std::cout << "noexcept olmayan tasima kurucusunda yeniden ayirma hic kopya kullandi mi: "
<< (GuvensizTasima::kopya_sayaci > 0) << "\n";
return 0;
}
noexcept tasima kurucusunda yeniden ayirma hic kopya kullandi mi: false noexcept tasima kurucusunda yeniden ayirma tasima kullandi mi: true noexcept olmayan tasima kurucusunda yeniden ayirma hic kopya kullandi mi: true
CB40. GuvenliTasima’nın taşıma kurucusu noexcept: vector yeniden ayırırken
kopyayı hiç kullanmıyor — kopya_sayaci > 0 false, tasima_sayaci > 0 true.
GuvensizTasima’nınki noexcept değil: vector kopyalamayı seçiyor —
kopya_sayaci > 0 true. noexcept yalnız bir belge notu değil, standart
kütüphane bu sözleşmeye göre farklı kod çalıştırıyor; sözü tutmayıp fırlaması
std::terminate’i tetikler, o yol burada çalıştırılmadı. Sınıf kopya:
noexcept’in varlığı, elemanların kopya mı taşıma mı ile taşınacağını belirliyor.
Sınırlayıcı Ölçüm
Bu derste istisnalar hep tek ya da sıralı birkaç catch bloğuyla yakalandı;
catch (...) ile her tipin yakalanması ve aynı hiyerarşide kısmi eşleşme
kapsam dışı kaldı. noexcept işaretli bir işlevden gerçekten fırlatmanın
std::terminate‘i tetiklemesi ölçülmedi — program temiz bir çıkışla sonlanamayacağı
için sandbox’ta gösterilemiyor; yalnız sözleşmenin tutulduğu durum ölçüldü.
Kopya-değiştir-tokuş kalıbının asıl güvencesi — kopya kurucusunun kendisi
fırlarsa *this’in dokunulmamış kalması — burada yalnız dolaylı olarak ele alındı,
kopya kurucusunun gerçekten fırlattığı durum ayrıca çalıştırılmadı.
Özet
throw h;isimli bir yerel değişkeni istisna depolamasına bir kez kopyalıyor;catch (HataNesnesi h)bu depolanmış nesneyi değerle yakalayarak ikinci bir kopya daha çıkarıyor.catch (const HataNesnesi&)depolanmış nesneye doğrudan bağlanıyor, ek bir kopya çıkarmıyor.- Bir kurucunun gövdesinde fırlatılan istisna, o ana kadar kurulmuş üyeleri kuruluş sırasının tersiyle yıkıyor, ama nesnenin kendi yıkıcısını hiç çağırmıyor — kurucu tamamlanmadığı için.
- Doğrudan bir geçici nesne fırlatmak (
throw HataNesnesi();), isimli bir yerel fırlatmaktan bir kopya eksik üretiyor. - Bu dersin ölçtüğü ifade kopya sınıfında: bir istisnanın fırlatılma ve yakalanma biçimi, sonunda kaç kopyanın çıkarıldığını doğrudan belirliyor.
Sonraki Adım
Bu derste HataNesnesi::kopya_sayaci, static bir üye olarak her çağrıda aynı
sayacı paylaştı. Bir sonraki ders, Ad Alanları ve Kod Düzeni, aynı ada sahip iki
sembolün ad alanlarıyla nasıl ayrıştırıldığını ve static/extern bağlantısının bir
sembolü tek dosyaya mı hapsettiğini yoksa dosyalar arasında mı paylaştırdığını
ölçüyor — yine bir ifadenin hangi sembolle çalıştığı sorusu, ama artık sınıf değil
dosya sınırları üzerinden.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.