> 'Exception Handling' : Programlama dünyasında programcılar kod yazarlarken iki farklı mentalitede kod yazarlardı. Bir taraftan işi yapacak kodları yazarlarken, diğer taraftan da programlama hatalarına veya programın çalışma zamanında karşılaşma ihtimali olduğu problemler için kodlar yazarlardı. İşte bu tip ikinci grup hatalara karşı yazılan kodlar ise iki grupta ele alınabilir. Bunlar 'static assertion' ve 'dynamic assertion'. Yani sırası ile derleme ve çalışma zamanına yönelik hataların bulunması, ele alınmasına yönelik çalışmalar. >> 'dynamic assertion' : C dilindeki 'assert()' makrosu bu gruptadır. >> 'static assertion' : Aşağıdaki örneği inceleyelim; * Örnek 1, //.. this is a C code. int myArray[0]; // Bu bir sentaks hatasıdır çünkü büyüklüğü sıfır olan bir dizi olamaz. typedef int ar[0]; // Bu da bir sentaks hatasıdır çünkü büyüklüğü sıfır olan bir diziye eş isim verilemez. typedef int ar[sizeof(int) == 4]; // Eğer bizim sistemimizde 'int' veri büyüklüğü 4-byte ise bu ifade bu hale gelecektir => "typedef int ar[1];" // Bu durumda herhangi bir sorun yoktur. Fakat eğer veri büyüklüğü 2-byte ise ilgili ifade şu hale gelecektir; // "typedef int ar[0];". İşte bu durumda bizler sentaks hatası almış olacağız. // Fakat C11 ile dile yeni bir anahtar sözcük eklendir => _Static_assert(); _Static_assert(sizeof(int) == 4, "Yetersiz int veri büyüklüğü"); >> İşte 'Exception Handling' mekanizmasının esas amacı programın çalışma zamanında beklenmeyen durumlar ile karşılaşılması durumunda nasıl davranacağını belirlemektir. Örneğin bir dosyaya yazma işlemi yapacağız fakat dosya üçüncü kişiler tarafından silinmiş. İşte böyle bir senaryoda kendi kodumuzu değiştirmek problemi ÇÖZMEYECEKTİR. Burada devreye bahsedilen bu mekanizma girmektedir eğer oraya yönelik kod yazarsak. >> Bir fonksiyonumuz işini yapamadığında, geri bildirim olarak "Ben işimi yapamadım." manasına gelen veri döndürüyorsa bu tip durumlara genel olarak Geleneksel Hata İşleme yöntemleri denmektedir. Örneğin, fonksiyonun dökümantasyonunda "sıfır" değerinin döndürülmesi "Başarı", "non-zero" değerin döndürülmesi "Başarısızlık" anlamının taşıdığı yazması. C dilindeki standart fonksiyonlarda genel olarak bu konvensiyon izlenir. Bir diğer örnek olarak C dilindeki 'fopen()', 'malloc()', 'setlocale()' fonksiyonları geri dönüş değerini kullanarak hata bilgisini döndürmektedir. Bu yaklaşıma alternatif bir diğer yol ise 'errno/errno.h' başlık dosyasını kullanmak. Çünkü bazı fonksiyonlar hata durumunda, bu başlık dosyasındaki global değişkeni 'set' ediyorlar. Fakat zaman zaman bu yaklaşım işimizi görmemektedir. Aşağıdaki örnekleri inceleyelim: * Örnek 1, 'errno.h' başlık dosyasının kullanılması ve geleneksel hata ayıklama yöntemleri: #include #include #include int main() { printf("errno : %d\n", errno); // OUTPUT => errno : 0 FILE* filePtr = fopen("YokBoyleBirDosya.txt", "r"); // Geleneksel hata işleme yöntemi ile; if(!filePtr) { printf("[YokBoyleBirDosya.txt] isimli boyle bir dosya YOK.\n"); // OUTPUT => [YokBoyleBirDosya.txt] isimli boyle bir dosya YOK. } // 'errno' kullanırsak printf("errno : %d\n", errno); // OUTPUT => errno : 2 // Buradaki '2' rakamı, hata koduna karşılık gelmektedir. İlgili fonksiyonun dökümanından // detaylarına bakılabilir. // Hata kodunu yazdirmak istersek; perror("hata "); // OUTPUT => hata : No such file or directory // Çıktıdan da görüldüğü üzere ilgili hata kodunun detaylarını yazdırmaktadır. printf("2 rakamina karsilik gelen hata kodu : %s", strerror(errno)); // OUTPUT => 2 rakamina karsilik gelen hata kodu : No such file or directory puts(strerror(errno)); // OUTPUT => No such file or directory // Buradaki 'strerror' isimli fonksiyon 'string.h' başlık dosyasında bildirilmiş ve ilgili hata kodunu bir // yaziya dönüştürmektedir. } * Örnek 2, Aynı hata kodunun iki farklı durumda döndürülmesi. #include #include int main() { char str[100]; printf("bir yazi girin : "); scanf("%s", str); // INPUT => Ahmet123, 123Ahmo, 0 int ival = atoi(str); printf("ival : %d\n", ival); // OUTPUT => 0, 123, 0 // Gördüğünüz gibi '0' rakamı 'output' olarak iki farklı senaryoda da kullanıldı. } >>> Örneklerden de görüleceği üzere Geleneksel Hata Ayıklama yöntemi aşağıdaki problemleri ve ihmallikleri de beraberinde getirebilir. >>>> İşi yapan kod ile işin yapılıp yapılmadığını test eden kod iç içe olduğundan, ilgili kodun okunması/test edilmesi/değiştirilmesi de bir hayli zorlaşmaktadır. Fakat bizim amacımız işi yapan kod ile hataları elden geçiren kodların birbirlerinden daha ayrık durumda olmaları ki işletme maliyeti azalsın. >>>> Bir hata durumunda fonksiyonu çağıran koda bu hata bilgisi geri döndürüldüğünden, o fonksiyon da tekrardan ilgili hata kodunu geri döndürebilir eğer hatayı çözme yetkisi kendisinde değilse. İç içe 15-20 defa fonksiyon çağrılma durumunda, belki de beşinci fonksiyona kadar bu hata kodu geri döndürülecektir.(her ne kadar C dilinde 'long-jump' mekanizmaları olsa da bunlar kodun okunmasını iyice zorlaştırmaktadır). Bu da bir işletme maliyeti doğurmaktadır. Hem bize 'long-jump' olanağı sunsa hem de okumayı kolaylaştıran bir mekanizma olsa çok daha iyi olurdu. >>>> Bir hatanın elden geçirilmesini zorlama olmadığından, gerek ihmalkarlık gerek başka nedenlerden, program hatalı bir şekilde çalışmaya devam edecektir. >> 'Exception Handling' mekanizması işletilirken beklenmeyen bir durum anında bir 'exception' nesnesi 'throw' edilir. Programın akışı bu aşamadan sonra, yani bir 'to throw exception' senaryosunda, bu hatayı yakalayan kod bloğuna geçiyor. Eğer herhangi bir koda tarafından yakalanmaması durumunda programımız SONLANIYOR. İşte C dilindeki Hata Ayıklama yöntemlerinin oluşturduğu sorunlar, ki bunlar yukarıda açıklanmıştır, C++ dilinde 'Exception Handling' mekanizması ile gideriliyor/hafifletiliyor. >> C dilinde hata durumlarında 'abort()' fonksiyonu çağrılmaktadır eğer duruma uygun ise. Fakat C++ dilinde ise 'std::terminate()' fonksiyonunu çağırmaktadır ki bu fonksiyon da arka planda 'abort()' fonksiyonunu çağırmaktadır. Fakat biz dilersek 'abort()' yerine kendi fonksiyonumuzu da çağırtabiliriz. >>> C dilindeki 'exit()' : terminates the process normally. Flushes unwritten buffered data. Closes all open files. Removes temporary files. Returns an integer exit status to the operating system. * Örnek 1, /* exit example */ #include #include int main () { FILE * pFile; pFile = fopen ("myfile.txt", "r"); if (pFile == NULL) { printf ("Error opening file"); exit (1); } else { /* file operations here */ } return 0; } >>> C dilindeki 'abort()' : Unlike exit() function, abort() may not close files that are open. It may also not delete temporary files and may not flush stream buffer. So programs like below might not write “Geeks for Geeks” to “tempfile.txt”. * Örnek 1, #include #include int main() { FILE *fp = fopen("C:\\myfile.txt", "w"); if(fp == NULL) { printf("\n could not open file "); getchar(); exit(1); } fprintf(fp, "%s", "Geeks for Geeks"); /* ....... */ /* ....... */ /* Something went wrong so terminate here */ abort(); getchar(); return 0; // If we want to make sure that data is written to files and/or buffers are flushed then we should either // use exit() or include a signal handler for SIGABRT. } >>> C dilindeki 'assert()' : Aşağıdaki örneğin inceleyelim. * Örnek 1, #include #include #define myTempAssert 35 void open_record(int ID) { assert(ID != myTempAssert); printf("No Assert!\n"); } int main(void) { open_record(25); // OUTPUT => No Assert! open_record(35); // OUTPUT => a.out: main.c:8: open_record: Assertion `ID != myTempAssert' failed. } >> Bir 'to throw an exception' senaryosu olduğunda, programın akışı başka yere kayacağından bahsetmiştik. İşte onun ispatı: * Örnek 1, Herhangi bir hata olmadığında programın akışı: #include void f4() { std::cout << "f4 cagrildi.\n"; std::cout << "f4 sona erdi.\n"; } void f3() { std::cout << "f3 cagrildi.\n"; f4(); std::cout << "f3 sona erdi.\n"; } void f2() { std::cout << "f2 cagrildi.\n"; f3(); std::cout << "f2 sona erdi.\n"; } void f1() { std::cout << "f1 cagrildi.\n"; f2(); std::cout << "f1 sona erdi.\n"; } int main() { /* # OUTPUT # main cagrildi. f1 cagrildi. f2 cagrildi. f3 cagrildi. f4 cagrildi. f4 sona erdi. f3 sona erdi. f2 sona erdi. f1 sona erdi. main sona erdi. */ std::cout << "main cagrildi.\n"; f1(); std::cout << "main sona erdi.\n"; } * Örnek 2, bir hata durumunda(uncaught exception): Bu durumda 'std::terminate()' fonksiyonu çağrılır. Bu fonksiyon varsayılan fonksiyon olarak 'abort()' çağrırır. #include void f4() { std::cout << "f4 cagrildi.\n"; throw 13; std::cout << "f4 sona erdi.\n"; } void f3() { std::cout << "f3 cagrildi.\n"; f4(); std::cout << "f3 sona erdi.\n"; } void f2() { std::cout << "f2 cagrildi.\n"; f3(); std::cout << "f2 sona erdi.\n"; } void f1() { std::cout << "f1 cagrildi.\n"; f2(); std::cout << "f1 sona erdi.\n"; } int main() { /* # OUTPUT # main cagrildi. f1 cagrildi. f2 cagrildi. f3 cagrildi. f4 cagrildi. terminate called after throwing an instance of 'int' */ std::cout << "main cagrildi.\n"; f1(); std::cout << "main sona erdi.\n"; // Çıktıtan da görüldüğü üzere bir hata gönderildikten sonra programın akışı artık başka yere geçmektedir. } >> Herhangi bir hatanın yakalanmaması durumunda 'std::terminate()' fonksiyonunun, onun da arka planda 'abort()' fonksiyonunu çağırdığını göstermiştik. İşte 'std::terminate' fonksiyonunun temsili implementasyonu. * Örnek 1, typedef void (*terminate_handler)(void); // Temsili gösterimi. terminate_handler set_terminate(terminate_handler); // Bizim istediğimiz fonksiyonun çağrılması için o fonksiyonu buraya argüman olarak geçmemiz gerekiyor. // Dolayısıyla eğer bu 'std::terminate' fonksiyonunu tekrar fabrika ayarına geri döndürmek için, bu fonksiyonun // geri dönüş değerini bir başka yerde SAKLAMAMIZ gerekiyore. * Örnek 2, #include #include void myCustomAbort() { std::cerr << "Yakalanamayan hata......\nabort() fonksiyonu cagrilicaktir.\n"; abort(); } void f4() { std::cout << "f4 cagrildi.\n"; throw 13; std::cout << "f4 sona erdi.\n"; } void f3() { std::cout << "f3 cagrildi.\n"; f4(); std::cout << "f3 sona erdi.\n"; } void f2() { std::cout << "f2 cagrildi.\n"; f3(); std::cout << "f2 sona erdi.\n"; } void f1() { std::cout << "f1 cagrildi.\n"; f2(); std::cout << "f1 sona erdi.\n"; } int main() { /* # OUTPUT # main cagrildi. f1 cagrildi. f2 cagrildi. f3 cagrildi. f4 cagrildi. Yakalanamayan hata...... abort() fonksiyonu cagrilicaktir. */ auto defaultAbort = std::set_terminate(myCustomAbort); std::cout << "main cagrildi.\n"; f1(); std::cout << "main sona erdi.\n"; // Çıktıktan da görüldüğü üzere yakalanamayan hata durumunda bizim fonksiyonumuz çağrılmıştır. } >> 'try-catch' Mekanizması: Aşağıdaki örneği inceleyelim. * Örnek 1, Kaba hali ile try-catch bloğu: //.. int main() { //.. try { /* Ama bu blok içerisinde bir şey 'throw' ediliyor olsun Ama bu blok içerisindeki fonksiyon içerisinden bir şey 'throw' ediliyor olsun Bu blokta çalışan kod 'throw' ettiğini varsayalım; */ } catch(/* throw edilen şey ile aynı türü buraya yazıyoruz */) { /* Eğer 'throw' edilen şeyin türü ile 'catch()' fonksiyonunun parametre parantezi içerisindeki tür aynı ise programın akışı buraya giriyor. */ } catch(/* throw edilen şey başka bir ise onun türünü de buraya yazıyoruz */) { /* Eğer 'throw' edilen şeyin türü ile bu 'catch()' fonksiyonunun parametre parantezi içerisindeki tür aynı ise programın akışı buraya giriyor. */ } catch(/*...*/) { /*...*/ } // Burada 'throw' edilen şey 'primitive' tür de olabilir, bu iş için oluşturulan özel bir sınıf da olabilir. // 'primitive' tür olması durumunda, 'catch' fonksiyonlarının parametreleri için bir tür dönüşümü SÖZ KONUSU // DEĞİLDİR. Yani, gönderilen tür 'float' ise onu yakalayacak 'catch()' fonksiyonunun parametre türünün de // 'float' OLMASI GEREKİYORE. } * Örnek 2, #include int main() { // OUTPUT => catch(float) try { throw 34.43f; } catch(int) { std::cout << "catch(int)\n"; } catch(double) { std::cout << "catch(double)\n"; } catch(float) { std::cout << "catch(float)\n"; } } * Örnek 3, #include int main() { // OUTPUT => terminate called after throwing an instance of 'float' try { throw 34.43f; } catch(int) { std::cout << "catch(int)\n"; } catch(double) { std::cout << "catch(double)\n"; } } >>> Eğer gönderilen şey yakalanırsa programın akışı ilgili 'catch()' fonksiyonunun bloğuna giriyor. Sonrasında da en sonki 'catch()' bloğunun bittiği yere geçiyor. * Örnek 1, #include #include void f4() { std::cout << "f4 cagrildi.\n"; throw 13; std::cout << "f4 sona erdi.\n"; } void f3() { std::cout << "f3 cagrildi.\n"; f4(); std::cout << "f3 sona erdi.\n"; } void f2() { std::cout << "f2 cagrildi.\n"; f3(); std::cout << "f2 sona erdi.\n"; } void f1() { std::cout << "f1 cagrildi.\n"; f2(); std::cout << "f1 sona erdi.\n"; } int main() { /* # OUTPUT # main cagrildi. f1 cagrildi. f2 cagrildi. f3 cagrildi. f4 cagrildi. Hata yakalandi. => x : 13 main sona erdi. */ std::cout << "main cagrildi.\n"; try{ f1(); } catch(int x) // 'try' bloğu içerisinde 'throw' edilen şeyin türü 'int' olduğundan, buraya da 'int' yazdık. { std::cout << "Hata yakalandi. => x : " << x << "\n"; } std::cout << "main sona erdi.\n"; // Çıktıda da görüldüğü üzere hata yakalandıktan sonra en sonki 'catch' bloğundan sonraki noktaya // geçmiştir. } * Örnek 2, yukarıda da anlatıldığı gibi ilgili hatanın yakalanabilmesi için 'throw' edilen şeyin türü ile 'catch()' fonksiyonunun parametresinin türünün aynı olması gerekmektedir. Bunun tek istisnası kalıtım hiyerarşisi. Türemiş sınıf türünden gönderilen bir şeyi taban sınıf ile yakalayabilirim. Onun haricinde 'Funciton Overload' mekanizmasındaki 'Exact-match' durumu. #include #include void f4() { std::cout << "f4 cagrildi.\n"; throw 13.31; std::cout << "f4 sona erdi.\n"; } void f3() { std::cout << "f3 cagrildi.\n"; f4(); std::cout << "f3 sona erdi.\n"; } void f2() { std::cout << "f2 cagrildi.\n"; f3(); std::cout << "f2 sona erdi.\n"; } void f1() { std::cout << "f1 cagrildi.\n"; f2(); std::cout << "f1 sona erdi.\n"; } int main() { /* # OUTPUT # main cagrildi. f1 cagrildi. f2 cagrildi. f3 cagrildi. f4 cagrildi. terminate called after throwing an instance of 'double' */ std::cout << "main cagrildi.\n"; try{ f1(); } catch(int x) { std::cout << "Hata yakalandi. => x : " << x << "\n"; } std::cout << "main sona erdi.\n"; // Hata yakalanamadığı için 'abort()' fonksiyonu çağrıldı. } >>> 'try' ve 'catch' blokları da birer blok olduklarından 'block-scope' KURALLARINA TABİİDİR. >>> 'throw' edilen şeyler çok az olasılık ile 'primitive' türler olmasına karşın ekseri çoğunluk standart kütüphanenin sunmuş olduğu hata sınıfı ile bizlerin oluşturduğu hata sınıflarından türlerdir. >>> Standart kütüphanenin öğelerinin göndermiş oldukları şeylerin 'std::exception' sınıfına veya o sınıftan türetilen sınıflara ait olduğu GARANTİ ALTINDADIR. Peki bu sınıf/sınıfları inceleyecek olursak; /* std::exception / | \ bad_alloc logic_error | out_of_range */ Buradan da görüleceği üzere; >>>> Standart kütüphanenin göndereceği her bir hata nesnesini, 'std::exception' sınıf türünden parametreye sahip bir 'catch()' fonksiyonu ile yakalayabilirim. >>>> Yine 'out_of_range' türden bir hata nesnesi gönderildiğinde, bu hata nesnesini, gerek 'out_of_range' sınıf türünden parametreye sahip gerek 'logic_error' sınıf türünden parametreye sahip gerek 'std::exception' sınıf türünden parametreye sahip bir 'catch()' fonksiyonu ile yakalayabilirim. İşte kalıtım hiyerarşisinin önemi burada da ortaya çıkmış oldu. Her 'out_of_range' bir 'logic_error' ve her bir 'logic_error' da bir 'std::exception'. * Örnek 1, #include #include #include void f4() { std::cout << "f4 cagrildi.\n"; std::string name{"ali"}; auto c = name.at(123); // İlgili fonksiyon, geçersiz indeks girildiğinde, 'out_of_range' sınıf türünden hata göndermektedir. std::cout << "f4 sona erdi.\n"; } void f3() { std::cout << "f3 cagrildi.\n"; f4(); std::cout << "f3 sona erdi.\n"; } void f2() { std::cout << "f2 cagrildi.\n"; f3(); std::cout << "f2 sona erdi.\n"; } void f1() { std::cout << "f1 cagrildi.\n"; f2(); std::cout << "f1 sona erdi.\n"; } int main() { /* # OUTPUT # main cagrildi. f1 cagrildi. f2 cagrildi. f3 cagrildi. f4 cagrildi. Hata yakalandi. => basic_string::at: __n (which is 123) >= this->size() (which is 3) main devam ediyor. f1 cagrildi. f2 cagrildi. f3 cagrildi. f4 cagrildi. Hata yakalandi. => basic_string::at: __n (which is 123) >= this->size() (which is 3) main sona ermek üzere. f1 cagrildi. f2 cagrildi. f3 cagrildi. f4 cagrildi. Hata yakalandi. => basic_string::at: __n (which is 123) >= this->size() (which is 3) main sona erdi. */ std::cout << "main cagrildi.\n"; try{ f1(); } catch(const std::out_of_range& exception) // { std::cout << "Hata yakalandi. => " << exception.what() << "\n"; } std::cout << "main devam ediyor.\n"; try{ f1(); } catch(const std::logic_error& exception) // { std::cout << "Hata yakalandi. => " << exception.what() << "\n"; } std::cout << "main sona ermek üzere.\n"; try{ f1(); } catch(const std::exception& exception) // { std::cout << "Hata yakalandi. => " << exception.what() << "\n"; } std::cout << "main sona erdi.\n"; // '.what()' fonksiyonu, en tepedeki 'std::exception' sınıfının bir sanal fonksiyonudur. // 'virtual-dispatch' mekanizması burada da işlemektedir. Fakat yazdırdığı yazının ne olacağı garanti // altında değildir. // Kalıtım hiyerarşisindeki üç farklı sınıf türü ile de aynı hatayı yakalamış olduk. Fakat // unutulmamalıdır ki 'exception' parametreli 'catch' bloğu standart kütüphanedeki bütün hataları // yakalayabilirken, 'out_of_range' parametreli 'catch' bloğu sadece 'out_of_range' türden hataları // yakalamaktadır. Yelpazeyi ne kadar geniş tutarsak, hatanın detayları konusunda da o kadar az detay // bilgiye sahip olabiliriz veya başka hataları yakalayamayabiliriz. Eğer aynı hata türü için birden // fazla 'catch' bloğu yazacaksak, ÖZELDEN GENELE DOĞRU YAZMALIYIZ. DAHA ÖZEL OLAN DAHA YUKARIDA // OLMALI. } * Örnek 2, ÖZELDEN GENELE DOĞRU SIRALAMA: #include #include #include void f4() { std::cout << "f4 cagrildi.\n"; std::string name{"ali"}; auto c = name.at(123); // İlgili fonksiyon, geçersiz indeks girildiğinde, 'out_of_range' sınıf türünden hata göndermektedir. std::cout << "f4 sona erdi.\n"; } void f3() { std::cout << "f3 cagrildi.\n"; f4(); std::cout << "f3 sona erdi.\n"; } void f2() { std::cout << "f2 cagrildi.\n"; f3(); std::cout << "f2 sona erdi.\n"; } void f1() { std::cout << "f1 cagrildi.\n"; f2(); std::cout << "f1 sona erdi.\n"; } int main() { /* # OUTPUT # main cagrildi. f1 cagrildi. f2 cagrildi. f3 cagrildi. f4 cagrildi. Hata yakalandi. => basic_string::at: __n (which is 123) >= this->size() (which is 3) main sona erdi.. */ std::cout << "main cagrildi.\n"; try{ f1(); } catch(const std::out_of_range& exception) // { std::cout << "Hata yakalandi. => " << exception.what() << "\n"; } catch(const std::logic_error& exception) // { std::cout << "Hata yakalandi. => " << exception.what() << "\n"; } catch(const std::exception& exception) // { std::cout << "Hata yakalandi. => " << exception.what() << "\n"; } std::cout << "main sona erdi.\n"; } >>> 'catch()' bloğundaki referanslık, neye referanslık? Derleyici nasıl bir üretmektedir? * Örnek 1, #include int main() { /* # OUTPUT # x : 10 */ int x = 10; try{ throw x; } catch(int& ref) { std::cout << "hata yakalandi.\n"; ref = 100; } std::cout << "x : " << x << "\n"; // Görüldüğü üzere 'x' değişkeninin değeri değişmedi. Demek ki 'x' değişkenine referans değilmiş. // Peki 'catch()' bloğundaki referans neye referans? // 'throw' deyimi ile derleyici ne yapıyor: // throw expression; // Derleyici derleme zamanında 'expression' isimli ifadenin türüne bakıyor. Bu türden bir nesne // oluşturuyor, bizim kod içerisinde görmediğimiz. Oluşturduğu bu nesneye ise 'expression' isimli ifade // ile ilk değer veriyor. // Pseudo code : // decltype(expression) myHiddenObject(expression); // Daha sonra derleyici 'myHiddenObject' nesnesini gönderiyor. Dolayısıyla 'catch' bloğundaki referans // da buna referans oluyor. // Peki oluşturulan bu gizli değişkenin ömrü ne zaman bitiyor? // Gönderilen hata 'handle' edildikten sonra da ömrü bitiyor. } * Örnek 2, yukarıdaki örnekteki senaryonun ispatı: #include class A{ public: A(){ std::cout << "A::A() this => " << this << "\n"; } A(const A&) { std::cout << "A::A(const A&) this => " << this << "\n"; } ~A(){ std::cout << "A::~A() this => " << this << "\n"; } }; void func() { std::cout << "func basladi.\n"; A ax; std::cout << "func devam ediyor.\n"; throw ax; } int main() { /* # OUTPUT # main basladi. func basladi. A::A() this => 0x7ffc96f5a567 // 'ax' isimli nesne için. func devam ediyor. A::A(const A&) this => 0x55754b5e0340 // Derleyicinin oluşturduğu gizli nesne için. A::~A() this => 0x7ffc96f5a567 // 'ax' isimli nesne için. hata yakalandi. A::~A() this => 0x55754b5e0340 // Derleyicinin oluşturduğu gizli nesne için. main sona erdi. */ std::cout << "main basladi.\n"; try{ func(); } catch(A& refA) { std::cout << "hata yakalandi.\n"; } std::cout << "main sona erdi.\n"; // Çıktıdan da görüldüğü üzere derleyicinin oluşturduğu gizli nesnenin hayatı, ilgili hata handle // edildikten sonra sona ermiştir. } >>> 'catch()' bloğu tek parametre kabul etmektedir. >>> 'catch()' bloğunda yakalama yaparken sınıf türünün kendisini kullanmamalıyız. O sınıf türüne REFERANS yolu ile yakalamalıyız. Aksi halde, >>>> Kopyalamanın maliyetinden kaçamamış oluruz. >>>> İlgili sınıf türü de kopyalanacağı için ona ait 'Copy Ctor' fonksiyonu da 'exception' gönderebilir. >>>> 'Object-slicing' senaryosu gerçekleşebilir, 'virtual-dispatch' MEKANİZMASI DA ÇALIŞMAYACAKTIR. >>> Kilit mesaj " throw by copy, catch by referance ". Yine 'throw' ederken de Geçici Nesne kullanmaya özen göstermeliyiz. İsimlendirilmiş nesnelerin 'throw' edilmeleri göze HOŞ GELMEMEKTEDİR. Örneğin, "throw std::out_of_range{"Aralik disi deger!!!..."};" şeklinde göndermeye özen göstermeliyiz. >>> 'catch()' bloğunda parantez içerisinde '...' atomunu koymak hata nesnesinin türünden bağımsız bütün hataları yakalamak içindir(%98). Fakat yakalamış olduğumuz hata nesnesinin ne olduğunu anlama şansımız yoktur. Sadece ne OLMADIĞINI anlayabiliriz; girmediği 'catch()' blokları, o türden olmadığı anlamındadır. * Örnek 1, #include #include void f4() { std::cout << "f4 cagrildi.\n"; throw 31; std::cout << "f4 sona erdi.\n"; } void f3() { std::cout << "f3 cagrildi.\n"; f4(); std::cout << "f3 sona erdi.\n"; } void f2() { std::cout << "f2 cagrildi.\n"; f3(); std::cout << "f2 sona erdi.\n"; } void f1() { std::cout << "f1 cagrildi.\n"; f2(); std::cout << "f1 sona erdi.\n"; } int main() { /* # OUTPUT # f1 cagrildi. f2 cagrildi. f3 cagrildi. f4 cagrildi. catch all... main cagrildi. main sona erdi. */ try{f1();} catch(const std::out_of_range& ex) { std::cout << "Hata yakalandi. ex => " << ex.what() << "\n"; } catch(const std::logic_error& ex) { std::cout << "Hata yakalandi. ex => " << ex.what() << "\n"; } catch(const std::exception& ex) { std::cout << "Hata yakalandi. ex => " << ex.what() << "\n"; } // Eğer bu blokta yakalanırsa şundan eminiz ki bizim hatamız standart sınıftaki hata sınıfından veya o // sınıftan türetilen bir sınıfa ait değil. catch(...){ std::cout << "catch all...\n"; } std::cout << "main cagrildi.\n"; std::cout << "main sona erdi.\n"; } >> Peki ne türden hatalar göndermeliyiz? El-cevap: Bu sorunun cevabı ekseriyet ile projenin dökümanlarında belirtilmiştir. Aksi belirtilmedikçe o dökümanları takip etmeliyiz. Fakat ekseriyet ile aşağıdaki türlerden hatalar gönderilmektedir. >>> Doğrudan 'std::exception' kalıtım hiyerarşisinde bulunan standart sınıflar türünden hata nesneleri, >>> Kendi oluşturduğumuz hata sınıfımız/sınıflarımız türünden hata nesnesi göndermek. >>>> Sıfırdan kendi sınıfımızı yazarak, >>>> Standart kütüphanenin hata sınıflarının kalıtım yolu ile yeni bir sınıf türeterek, * Örnek 1, #include #include #include // Standart kütüphanedeki bazı hata ayıklama fonksiyonları da burada bildirilmiştir. class A : public std::exception{ // 'std::exception' sınıfındaki 'what()' fonksiyonu 'overload' edilmez ise 'I' numaralı // çıktıyı alacağız. public: // Eğer onu 'overload' edersek, 'II' numaralı çıktıyı alacağız. const char* what()const noexcept override { return "Gecersiz data verisi.\n"; } }; int main() { /* # OUTPUT # hata yakalandi => std::exception // I hata yakalandi => Gecersiz data verisi. // II */ try{ throw A{}; } catch(std::exception& ex) { std::cout << "hata yakalandi => " << ex.what() << "\n"; } } >>>> Kullanmış olduğumuz üçüncü parti kütüphanelerinin hata sınıflarını kullanmak veya onlardan kendimize yeni sınıflar türeterek. >> Nerede müdahale etmek istiyorsak, 'exception' LARI ORADA YAKALAMAYA ÇALIŞMALIYIZ. Eğer bizler müdahale etmeyeceksek, herhangi bir hata yakalamaya ÇALIŞMAMALIYIZ. >> Bir şekilde bir hata yakalamış olalım. Önümüzde bizi bekleyen şeyler nelerdir? El-cevap, >>> Hatayı 'handle' eder ve programın çalışmaya devam etmesini veya programın kontrollü bir şekilde sonlanmasını sağlar. >>> Hatayı yakalar fakat başka bir sınıf türünden hata gönderir. (Hatayı 'translate' eder.) >>> Hatayı 'rethrow' eder. >> 'Uncaught-exception' durumunun zararları: RAII deyimi güden bir sınıfımız olsun. İsmi de A. Bu sınıfın kurucu işlevleri ilgili sınıf türünden nesneler hayata gelirken belli bir 'memory' ve 'resource' kullanmaktalar. İlgili nesnenin de hayatı bittiğinde kullanmış oldukları bu 'memory' ve 'resource' ları geri vermekteler. Programımızın çalışması esnasında bir hata fırlatılsa fakat bir nedenden dolayı yakalanamasa, 'abort()' fonksiyonu çağrılacaktır varsayılan ayar olaraktan. İş bu fonksiyon ise kontrolsiz bir şekilde programı sonlandıracaktır. Dolayısıyla sınıf nesnelerimiz elde etmiş oldukları 'memory' ve 'resource' leri kontrollü bir şekilde geri verememiş oldular. Bu da veri kaybına yol açabilir. Fakat eğer bizler bu hatayı yakalarsak dilin bize garanti etmiş olduğu 'stack-unwinding' mekanizması devreye girecektir. Bu mekanizma, hatanın gönderildiği yer ile hatanın ele alınacağı yer arasındaki 'yerel' değişkenlerin 'Dtor' fonksiyonlarının çağrılması mekanizmasıdır. 'catch()' bloğuna girmeden evvel bu 'Dtor' fonksiyonları çağrılmaktadır ve sadece 'Dtor' için de sınırlı değildir. 'Yerel' değişkenlerin ömürleri bitiyor. * Örnek 1, #include #include class A{ public: A() { std::cout << this << " : A::A() cagrildi. X isimli dosya acildi." << "Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi.\n"; } ~A() { std::cout << this << " : A::~A() cagrildi. X isimli dosya kapatildi. " >> "Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi.\n"; } }; void f4() { std::cout << "f4 cagrildi.\n"; A aaaax; // throw 31; std::cout << "f4 sona erdi.\n"; } void f3() { std::cout << "f3 cagrildi.\n"; A aaax; f4(); std::cout << "f3 sona erdi.\n"; } void f2() { std::cout << "f2 cagrildi.\n"; A aax; f3(); std::cout << "f2 sona erdi.\n"; } void f1() { std::cout << "f1 cagrildi.\n"; A ax; f2(); std::cout << "f1 sona erdi.\n"; } int main() { /* # OUTPUT # (HERHANGİ BİR HATA OLMAMASI DURUMUNDA) f1 cagrildi. 0x7ffd8329db27 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-bytebuyuklugunda alan ayrildi. f2 cagrildi. 0x7ffd8329daf7 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f3 cagrildi. 0x7ffd8329dac7 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f4 cagrildi. 0x7ffd8329da97 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f4 sona erdi. 0x7ffd8329da97 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. f3 sona erdi. 0x7ffd8329dac7 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. f2 sona erdi. 0x7ffd8329daf7 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. f1 sona erdi. 0x7ffd8329db27 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. main cagrildi. main sona erdi. */ /* # OUTPUT # (f4() FONKSİYONU HATA GÖNDERDİ.) f1 cagrildi. 0x7ffc8adb9697 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f2 cagrildi. 0x7ffc8adb9667 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f3 cagrildi. 0x7ffc8adb9637 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f4 cagrildi. 0x7ffc8adb9607 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. terminate called after throwing an instance of 'int' */ f1(); std::cout << "main cagrildi.\n"; std::cout << "main sona erdi.\n"; // Çıktılardan da görüldüğü üzere elde etmiş olduğumuz kaynakları geri veremedik, açmış olduğumuz dosyaları // kapatamadık. Database bağlantılarını sonlandıramadık. } * Örnek 2, Stack Unwinding Mekanizmasının İşletilmesi: #include #include class A{ public: A() { std::cout << this << " : A::A() cagrildi. X isimli dosya acildi." << "Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi.\n"; } ~A() { std::cout << this << " : A::~A() cagrildi. X isimli dosya kapatildi." << "Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi.\n"; } }; void f4() { std::cout << "f4 cagrildi.\n"; A aaaax; throw 31; std::cout << "f4 sona erdi.\n"; } void f3() { std::cout << "f3 cagrildi.\n"; A aaax; f4(); std::cout << "f3 sona erdi.\n"; } void f2() { std::cout << "f2 cagrildi.\n"; A aax; f3(); std::cout << "f2 sona erdi.\n"; } void f1() { std::cout << "f1 cagrildi.\n"; A ax; f2(); std::cout << "f1 sona erdi.\n"; } int main() { /* # OUTPUT # f1 cagrildi. 0x7fff7d99d3c7 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f2 cagrildi. 0x7fff7d99d397 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f3 cagrildi. 0x7fff7d99d367 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f4 cagrildi. 0x7fff7d99d337 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. 0x7fff7d99d337 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. 0x7fff7d99d367 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. 0x7fff7d99d397 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. 0x7fff7d99d3c7 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. catch(int)....... main cagrildi. main sona erdi. */ try{ f1(); } catch(int) { std::cout << "catch(int).......\n"; } std::cout << "main cagrildi.\n"; std::cout << "main sona erdi.\n"; } * Örnek 3, RAII deyimi ve Stack Unwinding mekanizması: #include #include class A{ public: A() { std::cout << this << " : A::A() cagrildi. X isimli dosya acildi." << "Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi.\n"; } ~A() { std::cout << this << " : A::~A() cagrildi. X isimli dosya kapatildi." << "Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi.\n"; } }; void f4() { std::cout << "f4 cagrildi.\n"; A aaaax; throw 31; std::cout << "f4 sona erdi.\n"; } void f3() { std::cout << "f3 cagrildi.\n"; A aaax; f4(); std::cout << "f3 sona erdi.\n"; } void f2() { std::cout << "f2 cagrildi.\n"; A aax; f3(); std::cout << "f2 sona erdi.\n"; } void f1() { std::cout << "f1 cagrildi.\n"; A ax; f2(); std::cout << "f1 sona erdi.\n"; } void func() { FILE* filePTR = fopen("deneme.txt", "w"); //.. fprintf(filePTR, "asjdflhgfkjhdkflghdlkfjgh"); f1(); fclose(filePTR); } int main() { /* (f4() fonksiyonu hata gönderdi ama YAKALANMADI. Ek olarak oluşturulan 'deneme.txt' dosyasına da bir şey yazılamadı.) # OUTPUT # f1 cagrildi. 0x7ffc89cf0807 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f2 cagrildi. 0x7ffc89cf07d7 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f3 cagrildi. 0x7ffc89cf07a7 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f4 cagrildi. 0x7ffc89cf0777 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. terminate called after throwing an instance of 'int' */ /* (f4() fonksiyonu hata gönderdi ve YAKALANDI. Ek olarak oluşturulan 'deneme.txt' dosyasına da belirtilen yazı yazıldı.) # OUTPUT # f1 cagrildi. 0x7ffde8a53c07 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f2 cagrildi. 0x7ffde8a53bd7 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f3 cagrildi. 0x7ffde8a53ba7 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f4 cagrildi. 0x7ffde8a53b77 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. 0x7ffde8a53b77 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. 0x7ffde8a53ba7 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. 0x7ffde8a53bd7 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. 0x7ffde8a53c07 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. catch(int)....... main cagrildi. main sona erdi. */ try{ func(); } catch(int) { std::cout << "catch(int).......\n"; } std::cout << "main cagrildi.\n"; std::cout << "main sona erdi.\n"; // Her iki senaryoda da programın akışı "fclose(filePTR);" satırına hiç bir zaman gelmemektedir. Çünkü hata // gönderildikten sonra ilgili 'catch()' bloğuna geçmekte, oradan da bir alt satırdan devam etmektedir. Eğer // bizler bu dosya açma-kapama işlemini bu kadar çıplak yapmak yerine RAII deyimi ile sarmalasaydık, // Stack-Unwinding gereği bir sorunumuz kalmayacaktı. } * Örnek 4, RAII deyimi ve Stack Unwinding mekanizması v2: #include #include class A{ public: A() { std::cout << this << " : A::A() cagrildi. X isimli dosya acildi." << "Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi.\n"; } ~A() { std::cout << this << " : A::~A() cagrildi. X isimli dosya kapatildi." << "Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi.\n"; } }; class FileHandler{ public: FileHandler(const char* p) : fp(fopen(p, "w")) { std::cout << "Dosya acildi.\n"; fprintf(fp, "qweroıuwqeroıu"); } ~FileHandler() { fclose(fp); std::cout << "Dosya kapatildi.\n"; } private: FILE* fp; }; void f4() { std::cout << "f4 cagrildi.\n"; A aaaax; throw 31; std::cout << "f4 sona erdi.\n"; } void f3() { std::cout << "f3 cagrildi.\n"; A aaax; f4(); std::cout << "f3 sona erdi.\n"; } void f2() { std::cout << "f2 cagrildi.\n"; A aax; f3(); std::cout << "f2 sona erdi.\n"; } void f1() { std::cout << "f1 cagrildi.\n"; A ax; f2(); std::cout << "f1 sona erdi.\n"; } void func() { FileHandler fObj("deneme.txt"); f1(); } int main() { /* (f4() hata gönderdi, yakalandi ve dosyaya da qweroıuwqeroıu yazildi.) # OUTPUT # Dosya acildi. f1 cagrildi. 0x7ffe834d2487 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f2 cagrildi. 0x7ffe834d2457 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f3 cagrildi. 0x7ffe834d2427 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f4 cagrildi. 0x7ffe834d23f7 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. 0x7ffe834d23f7 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. 0x7ffe834d2427 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. 0x7ffe834d2457 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. 0x7ffe834d2487 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. Dosya kapatildi. catch(int)....... main cagrildi. main sona erdi. */ try{ func(); } catch(int) { std::cout << "catch(int).......\n"; } std::cout << "main cagrildi.\n"; std::cout << "main sona erdi.\n"; // RAII deyiminin ne kadar da faydalı olduğunu bir kez daha görmüş olduk. Peki ilgili 'A' sınıfımız dinamik // olarak elde etseydik? İş bu Stack-Unwinding mekanizması yine ÇALIŞMAYACAKTI. Çünkü dinamik ömürlü nesneler // 'heap' alanında saklanmaktadırlar. İşte dinamik ömürlü nesnelere de RAII deyimini eklemek için Akıllı // Göstericileri KULLANMALIYIZ. Çünkü onlar da aslındaki yukarıdaki FileHandler sınıfı gibi birer sınıflar. } * Örnek 5, RAII, Stack-Unwinding ve Akıllı Göstericiler: #include #include class A{ public: A() { std::cout << this << " : A::A() cagrildi. X isimli dosya acildi." << "Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi.\n"; } ~A() { std::cout << this << " : A::~A() cagrildi. X isimli dosya kapatildi." << "Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi.\n"; } }; void f4() { std::cout << "f4 cagrildi.\n"; auto ptr = new A; throw 31; std::cout << "f4 sona erdi.\n"; delete ptr; } void f3() { std::cout << "f3 cagrildi.\n"; auto ptr = new A; f4(); std::cout << "f3 sona erdi.\n"; delete ptr; } void f2() { std::cout << "f2 cagrildi.\n"; auto ptr = new A; f3(); std::cout << "f2 sona erdi.\n"; delete ptr; } void f1() { std::cout << "f1 cagrildi.\n"; auto ptr = new A; f2(); std::cout << "f1 sona erdi.\n"; delete ptr; } int main() { /* (f4() hata gönderdi ve yakalandi. Ama dinamik ömürlü nesneler için 'Dtor' çağrılmadı.) # OUTPUT # f1 cagrildi. 0x562d0002b2c0 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f2 cagrildi. 0x562d0002b2e0 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f3 cagrildi. 0x562d0002b300 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f4 cagrildi. 0x562d0002b320 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. catch(int)....... main cagrildi. */ try{ f1(); } catch(int) { std::cout << "catch(int).......\n"; } std::cout << "main cagrildi.\n"; std::cout << "main sona erdi.\n"; // Fonksiyonların içerisindeki 'ptr' isimli şey bir sınıf nesnesidir. } * Örnek 6, RAII, Stack-Unwinding ve Akıllı Göstericiler v2: #include #include #include class A{ public: A() { std::cout << this << " : A::A() cagrildi. X isimli dosya acildi." << "Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi.\n"; } ~A() { std::cout << this << " : A::~A() cagrildi. X isimli dosya kapatildi." << "Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi.\n"; } }; void f4() { std::cout << "f4 cagrildi.\n"; auto ptr = std::make_unique(); throw 31; std::cout << "f4 sona erdi.\n"; } void f3() { std::cout << "f3 cagrildi.\n"; auto ptr = std::make_unique(); f4(); std::cout << "f3 sona erdi.\n"; } void f2() { std::cout << "f2 cagrildi.\n"; auto ptr = std::make_unique(); f3(); std::cout << "f2 sona erdi.\n"; } void f1() { std::cout << "f1 cagrildi.\n"; auto ptr = std::make_unique(); f2(); std::cout << "f1 sona erdi.\n"; } int main() { /* (f4() hata gönderdi ve yakalandi. Akıllı Göstericiler kullanıldığından, A sınıfı için de 'Dtor' çağrıldı.) # OUTPUT # f1 cagrildi. 0x56080fdb92c0 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f2 cagrildi. 0x56080fdb92e0 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f3 cagrildi. 0x56080fdb9300 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. f4 cagrildi. 0x56080fdb9320 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. 0x56080fdb9320 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. 0x56080fdb9300 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. 0x56080fdb92e0 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. 0x56080fdb92c0 : A::~A() cagrildi. X isimli dosya kapatildi. Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi. catch(int)....... main cagrildi. main sona erdi. */ try{ f1(); } catch(int) { std::cout << "catch(int).......\n"; } std::cout << "main cagrildi.\n"; std::cout << "main sona erdi.\n"; } >>> Hataların bir çoğunu 'main()' fonksiyonunun bloğunu kapsayıcı bir şekilde 'try' bloğu içine alırsak yakalarız. Fakat 'global namespace' alanındaki değişkenlerin 'Ctor' fonksiyonları hata gönderirse ONLARI YAKALAYAMAYIZ. Çünkü bu nesneler 'main()' fonksiyonundan önce hayata gelmektedirler. Dolayısıyla bu hataları yakalamak için başka yöntemler denemeliyiz. * Örnek 1, #include #include class A{ public: A() { std::cout << this << " : A::A() cagrildi. X isimli dosya acildi." << "Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi.\n"; throw 31; } ~A() { std::cout << this << " : A::~A() cagrildi. X isimli dosya kapatildi." << "Y database baglantisi kesildi. 250-byte buyuklugunda alan geri verildi.\n"; } }; A ga; // Bu nesnenin kurucu işlevinin gönderdiği hata nesnesi YAKALANAMADI. Dolayısıyla 'abort()' çağrıldı. int main() { try // %99 ihtimal ile yakalamak için. { std::cout << "main cagrildi.\n"; /* # OUTPUT # 0x564155e71151 : A::A() cagrildi. X isimli dosya acildi. Y database baglantisi kuruldu. 250-byte buyuklugunda alan ayrildi. terminate called after throwing an instance of 'int' */ std::cout << "main sona erdi.\n"; } catch(...) { std::cout << "catch(...).......\n"; } } >> Exception Kalıtım Hiyerarşisi : En tepe sınıf "std::exception" sınıfıdır. Bu sınıftan sırasıyla, "bad_alloc", "bad_cast", "logic_error", "bad_function_call", "bad_typeid", "runtime_error", "bad_weak_ptr", "bad_exception" sınıfları türetilmiştir. Bu sınıflardan, >>> "bad_alloc" sınıfından "bad_array_new_length" sınıfı türetilmiştir. >>> "logic_error" sınıfından "domain_error", "length_error", "future_error", "out_of_range" ve "invalid_argument" sınıfları türetilmiştir. >>> "runtime_error" sınıfından "system_error", "range_error", "underflow_error" ve "overflow_error" sınıfları türetilmiştir. Bu türetilen sınıflardan "system_error" sınıfı aynı zamanda "ios_base::failure" sınıfı türetilmiştir. >> NOT: Exception Handling mekanizması, Asenktron Programlamaya yönelik doğrudan bir araç değildir. Tamamen Senkron Programlamaya yönelik bir araçtır. >> Ctor. ve Exceptions : Ctor. bir hata durumunda karşılaştığında EXCEPTION THROW ETMELİ, aksi bildirilmemişse. Eğer bir nesne hayata gelirken EXCEPTION THROW ETMEZ İSE HAYATA GELMİŞ OLACAK. ETMESİ DURUMUNDA HAYATA GELMEMİŞ OLACAK. Dolayısıyla kendisine ait Dtor. fonksiyonu da ÇAĞRILMAYACAK. Eğer iş bu sınıfımız nesne hayata getirirken raw-pointer ile kaynaklar elde ediyorsa, Dtor. fonksiyonu çağrılmayacağı için o KAYNAKLAR DA GERİ VERİLEMEYECEK. İşte bu yüzden Akıllı Pointer kullanmalıyız ki Stack-Unwinding mekanizmasından da yararlanalım. Diğer alternatifler ise Ctor. fonksiyonumuzun EXCEPTION THROW etmeyeceğinden %100 olmalıyız(ama hatayı Ctor. içinde yakalayarak ama hiç hata nesnesi göndermeyen kod yazarak), böylelikle raw-pointer kullanabiliriz. * Örnek 1, #include #include class A{ public: A() { std::cout << this<< " : A::A() cagrildi.\n"; throw 31; } ~A() { std::cout << this << " : A::~A() cagrildi.\n"; } }; int main() { /* # OUTPUT # 0x7fff84fa2433 : A::A() cagrildi. Hata yakalandi. */ try { A ax; } catch(int) { std::cout << "Hata yakalandi.\n"; } } * Örnek 2, Edinilen kaynakların geri verilememesi. #include #include class Nec{ public: Nec() { std::cout << this << " => Nec::Nec() cagrildi.\n"; } ~Nec() { std::cout << this << " => ~Nec::Nec() cagrildi.\n"; } }; class A{ public: A() { std::cout << this<< " : A::A() cagrildi.\n"; mp = new Nec[3]; throw 31; } ~A() { std::cout << this << " : A::~A() cagrildi.\n"; delete[] mp; } private: Nec* mp; }; int main() { /* # OUTPUT # 0x7ffef9134d10 : A::A() cagrildi. 0x564045abd2c8 => Nec::Nec() cagrildi. 0x564045abd2c9 => Nec::Nec() cagrildi. 0x564045abd2ca => Nec::Nec() cagrildi. Hata yakalandi. */ try { A ax; } catch(int) { std::cout << "Hata yakalandi.\n"; } // Her iki örnekteki çıktıdan da görüldüğü üzere A nesnesi hayata gelirken edinmiş olduğu kaynakları // geri veremedi. } * Örnek 3, Akıllı Gösterici ile kaynakların geri verilmesi. #include #include #include class Nec{ public: Nec() { std::cout << this << " => Nec::Nec() cagrildi.\n"; } ~Nec() { std::cout << this << " => ~Nec::Nec() cagrildi.\n"; } }; class A{ public: A() { std::cout << this<< " : A::A() cagrildi.\n"; mp = std::make_unique(); throw 31; } ~A() { std::cout << this << " : A::~A() cagrildi.\n"; } private: std::unique_ptr mp; }; int main() { /* # OUTPUT # 0x7fffec8838a0 : A::A() cagrildi. 0x558c4a7462c0 => Nec::Nec() cagrildi. 0x558c4a7462c0 => ~Nec::Nec() cagrildi. Hata yakalandi. */ try { A ax; } catch(int) { std::cout << "Hata yakalandi.\n"; } // Akıllı Gösterici kullanmamız ve Stack-Unwinding mekanizması sayesinde 'ax' nesnesi hayata gelirken elde // etmiş olduğu kaynakları geri verdi. } >>> Dinamik ömürlü bir nesne hayata gelirken ona ait Ctor. fonksiyonu 'exception throw' ederse ve yakalanırsa, elde etmiş olduğu BELLEK BLOĞUNUN GERİ VERİLME GARANTİSİ VARDIR. O nesnenin Dtor. fonksiyonu yine çağrılmayacaktır. * Örnek 1, #include #include #include void* operator new(size_t n) { std::cout << "operator new(" << n << ") was called.\n"; void* vp = std::malloc(n); std::cout << "A memory block has been occupied starting from : " << vp << "\n"; if(!vp) throw std::bad_alloc{}; return vp; } void operator delete(void* vp) { std::cout << "operator delete() was called.\n"; std::cout << "A memory block will be given back, starting from : " << vp << "\n"; if(vp) free(vp); } class A{ public: A() { std::cout << this<< " : A::A() cagrildi.\n"; throw 31; } ~A() { std::cout << this << " : A::~A() cagrildi.\n"; } private: char buffer[1024]; }; int main() { /* # OUTPUT # operator new(1024) was called. A memory block has been occupied starting from : 0x55ad338472c0 0x55ad338472c0 : A::A() cagrildi. operator delete() was called. A memory block will be given back, starting from : 0x55ad338472c0 Hata yakalandi. */ try { A* ptr = new A; } catch(int) { std::cout << "Hata yakalandi.\n"; } } >>> Veri elemanının bir sınıf türünden olması durumunda ise; * Örnek 1, #include #include #include class Member{ public: Member() { std::cout << "Member ctor.\n"; } ~Member() { std::cout << "Member dtor.\n"; } }; class A{ public: A() { std::cout << "A ctor.\n"; throw 31; } ~A() { std::cout << "A dtor.\n"; } private: Member mx; }; int main() { /* # OUTPUT # Member ctor. A ctor. Member dtor. Hata yakalandi. */ try { A ax; } catch(int) { std::cout << "Hata yakalandi.\n"; } // İlk önce A nesnesinin veri elemanları hayata geliyor. Sonrasında programın akışı A sınıfının Ctor. // fonksiyonunun bloğuna giriyor. Dolayısıyla 'mx' nesnesi çoktan hayata gelmiş durumda hata // gönderildiğinde. Stack-Unwinding mekanizması ile hayata gelmemiş 'ax' nesnesi içindeki hayata gelmiş // 'mx' nesnesinin Dtor. fonksiyonu çağrılmaktadır. // Peki hatayı Member sınıfı gönderseydi ne olacaktı? } * Örnek 2, Veri elemanının hata göndermesi #include #include #include class Member{ public: Member() { std::cout << "Member ctor.\n"; throw 31; } ~Member() { std::cout << "Member dtor.\n"; } }; class A{ public: A() { std::cout << "A ctor.\n"; } ~A() { std::cout << "A dtor.\n"; } private: Member mx; }; int main() { /* # OUTPUT # Member ctor. Hata yakalandi. */ try { A ax; } catch(int) { std::cout << "Hata yakalandi.\n"; } // A sınıfı içindeki Member da hayata gelmemiş olacağından, her ikisinin de Dtor. fonksiyonları // çağrılmayacaktı. Peki A sınıfımız birden fazla sınıf türünden veri elemanına sahip olursa? } * Örnek 3, Birden fazla veri elemanına sahip olunması durumunda #include #include #include class Member{ public: Member() { std::cout << "Member ctor.\n"; throw 31; } ~Member() { std::cout << "Member dtor.\n"; } }; class Data{ public: Data() { std::cout << "Data ctor.\n"; } ~Data() { std::cout << "Data dtor.\n"; } }; class A{ public: A() { std::cout << "A ctor.\n"; } ~A() { std::cout << "A dtor.\n"; } private: Data dx; Member mx; }; int main() { /* # OUTPUT # Data ctor. Member ctor. Data dtor. Hata yakalandi. */ try { A ax; } catch(int) { std::cout << "Hata yakalandi.\n"; } // A sınıfının veri elemanları, bildirimdeki sıra ile hayata geleceklerinden dolayı ilk önce 'dx' nesnesi // hayata geliyor. 'mx' nesnesi hayata gelme aşamasındayken hata gönderdiği için hayata gelemiyor. // Dolayısıyla sadece hayata 'dx' geldiğinden, ona ait Dtor. fonksiyonu çağrılıyor. } >> Exception ve Dtor. Fonksiyonları: Dtor. fonksiyonlarından dışarı bir EXCEPTION GONDERILMEMELI. Bu durumda ya biz Dtor. fonksiyonu içerisinde bu hatayı yakalamalıyız ya da hata göndermeyecek kod yazmalıyız. Çünkü dilin kuralları gereği bir exception 'handle' edilirken bir exception daha fırlatılırsa, 'std::terminate' fonksiyonu çağrılacaktır. Unutulmamalıdır ki Dtor. fonksiyonu iki farklı senaryoda çağrılmaktadır. Bunlardan ilki ömrü biten bir nesne için, ikinci ise Stack-Unwinding mekanizmasından dolayı. Günün sonunda da 'std::terminate' iki farklı senaryoda çağrılmaktadır. İlk senaryo gönderilen hatanın yakalanamaması durumunda(bkz. Uncaught Exception) ve Stack-Unwinding sürecinde, bir exception 'handle' edilirken Dtor. fonksiyonlardan birinin tekrar exception göndermesi durumunda. * Örnek 1, #include #include void myCustomAbort() { std::cerr << "Yakalanamayan hata......\n"; } void foo() { throw 31; } class Member{ public: Member() { std::cout << "Member ctor.\n"; } ~Member() { std::cout << "Member dtor.\n"; foo(); } }; void func() { Member mx; throw 32; } int main() { /* # OUTPUT # Member ctor. Member dtor. Yakalanamayan hata...... */ std::set_terminate(myCustomAbort); try { func(); } catch(int) { std::cout << "Hata yakalandi.\n"; } // Gördüğünüz üzere programın akışı catch bloğuna girmeden 'std::terminate' çağrıldı. Çünkü 'stack-unwinding' // sürecinde Dtor. tekrardan exception gönderdi. BURADAKİ KİLİT NOKTA Dtor. fonksiyonunun, dışarıya exception // göndermesi. Velevki biz Dtor. içerisinde bu hatayı 'handle' etseydik, dışarıya göndermeseydik, bir sorun // olmayacaktı. } >> 'rethrow' statement: 'throw' anahtar kelimesini ';' atomunun izlemesi durumudur. Böylelikle yakalanan hata, tekrardan gönderilmektedir. * Örnek 1, #include #include class MyExceptHandler{ public: inline static int mVal{30}; }; void func() { throw MyExceptHandler{}; } void foo() { try{ func(); } catch(MyExceptHandler& x) { std::cout << "Hata foo içinde yakalandi. x : " << x.mVal << "\n"; x.mVal = 15; // throw x; // 're-throw' YAPILMAMAKTADIR. Yeni bir hata nesnesi kopyalama yolu ile oluşturulmuştur ve o // gönderilmiştir. Daha önce yakalananın da hayatı sona ermiş oluyor. İki numaralı örnek incelenebilir. throw; // re-throw yapıldığı yer. } } int main() { /* # OUTPUT # Hata foo içinde yakalandi. x : 30 Hata main içinde yakalandi. x : 15 */ try { foo(); } catch(MyExceptHandler& x) { std::cout << "Hata main içinde yakalandi. x : " << x.mVal << "\n"; } } * Örnek 2, 'object-slicing' gerçekleşebilir. #include #include #include void func() { try{ std::string name{"Özge"}; auto c = name.at(123); } catch(const std::exception& ex) { std::cout << "Hata func içinde yakalandi. => " << ex.what() << "\n"; throw ex; // I } } int main() { /* # OUTPUT # Hata func içinde yakalandi. => basic_string::at: __n (which is 123) >= this->size() (which is 5) terminate called after throwing an instance of 'std::exception' what(): std::exception */ try { func(); } catch(const std::out_of_range& ex) { std::cout << "Hata main içinde yakalandi. => " << ex.what() << "\n"; } // Çıktıtan da görüldüğü üzere bizler ikinci defa gönderilen hatayı yakayalamadık. // Çünkü 'I' numaralı deyim yürütüldüğünde 'object-sliding' gerçekleşti, yeni bir nesne // kopyalama yoluyla oluşturuldu ve o tekrardan gönderildi. Bizlerin gönderilen bu ikinci nesneyi yakalaması // için 'main' işlevi içindeki 'catch()' parametresi de 'std::exception' türünden olması gerekmekteydi yada // 're-throw' yapmamız gerekiyordu. } * Örnek 3, 're-throw' yapıldığındaki sonuç: #include #include #include void func() { try{ std::string name{"Özge"}; auto c = name.at(123); } catch(const std::exception& ex) { std::cout << "Hata func içinde yakalandi. => " << ex.what() << "\n"; throw; } } int main() { /* # OUTPUT # Hata func içinde yakalandi. => basic_string::at: __n (which is 123) >= this->size() (which is 5) Hata main içinde yakalandi. => basic_string::at: __n (which is 123) >= this->size() (which is 5) */ try { func(); } catch(const std::out_of_range& ex) { std::cout << "Hata main içinde yakalandi. => " << ex.what() << "\n"; } } * Örnek 4, 'main' işlevindeki yakalama parametresinin değiştirilmesi:(object-sliding) #include #include #include void func() { try{ std::string name{"Özge"}; auto c = name.at(123); } catch(const std::exception& ex) { std::cout << "Hata func içinde yakalandi. => " << ex.what() << "\n"; throw ex; } } int main() { /* # OUTPUT # Hata func içinde yakalandi. => basic_string::at: __n (which is 123) >= this->size() (which is 5) Hata main içinde yakalandi. => std::exception */ try { func(); } catch(const std::exception& ex) { std::cout << "Hata main içinde yakalandi. => " << ex.what() << "\n"; } // Çıktıdan da görüldüğü üzere bir 'object-slicing' mevzusu gerçekleşmiş fakat HATA YİNE YAKALANMIŞTIR. } >> 're-throw' deyiminin doğrudan 'catch' bloğu içerisinde olma zorunlulu yoktur. Bu bloktan çağrılan fonksiyonların bloklarında da olabilir. #include #include void myFunc() { std::cout << "myFunc()\n"; throw; } void foo() { std::cout << "foo()\n"; myFunc(); } void func() { try{ throw 31; } catch(int ex) { std::cout << "func::catch(int " << ex << ")\n"; foo(); } } int main() { /* # OUTPUT # func::catch(int 31) foo() myFunc() Hata main içinde yakalandi. => 31 */ try { func(); } catch(int x) { std::cout << "Hata main içinde yakalandi. => " << x << "\n"; } // Peki bizler direkt olarak 'myFunc' fonksiyonunu çağırırsak ne olur? // Derleyici bir throw görmeden 'rethrow' görmesi durumunda 'std::terminate' fonksiyonunu çağıracaktır. // Böylelikle derleyicinin 'std::terminate' fonksiyonunu çağırdığı üç farklı senaryo olmuş oldu. Bunlar; // i. 'Uncaught-exception' durumunda, // ii. 'Stack-Unwinding' durumunda Dtor. fonksiyonunun tekrardan hata göndermesi durumunda, // iii. Derleyici bir hata nesnesinin fırlatıldığını görmeden evvel direkt olarak 're-throw' görmesi // durumunda. // Diğer yandan derleyiciler Dtor. fonksiyonunu da iki senaryoda çağrı yapmaktadır. Bunlar, // i. Ömrü biten nesneler için, // ii. 'Stack-Unwinding' durumunda otomatik ömürlü nesneler için. } >> 'Exception Dispatcher' deyiminin bir implementasyonu: * Örnek 1, #include #include class ExceptionTypeI{}; class ExceptionTypeII{}; class ExceptionTypeIII{}; void handle_exceptions() { try{ throw; // 'main' işlevindeki 'catch-all' bloğunda yakalanan hata tekrardan 're-throw' ediliyor. Böylelikle // aşağıdaki üç tane 'catch' bloklarından bir tanesi tarafından yakalanacağı umulmakta. } catch(const ExceptionTypeI& ex) { } catch(const ExceptionTypeII& ex) { } catch(const ExceptionTypeIII& ex) { } } int main() { /* # OUTPUT # func::catch(int 31) foo() myFunc() Hata main içinde yakalandi. => 31 */ try { // Buradan bir hata gönderildiğini varsayalım. } /* Burada standart kütüphanedeki Exception sınıfından parametre alan 'catch' bloklarının oldunu varsayalım.*/ catch(...) { // Dolayısıyla hata burada yakalanırsa, gönderilen hatanın standart kütüphanenin 'exception' sınıfına ait // olmadığı kesin. handle_exceptions(); } } >> 'Exception Guarantees' : Bir kodun 'exception-safe' olabilmesi için bazı koşulları sağlaması gerekmektedir. Unutulmamalıdır ki bir fonksiyon kodunu yazarken 'exception throw' edilmesi durumunda hiç bir şekilde bir kaynak sızıntısına mahal vermemelidir. Aşağıdaki örneği inceleyelim. * Örnek 1, // Aşağıdaki 'func' fonksiyonunu biz yazıyor olalım. void func(int n) { auto p = new int[n]; // some other codes here. // Farz edelim ki burada yazdığımız bir başka kod 'exception' throw etti. // some other codes here... delete p; } // Artık yukarıdaki fonksiyon bloğunda elde etmiş olduğumuz bellek bloğuna erişme ihtimalimiz YOKTUR. İşte bu // fonksiyon 'exception-safe' bir fonksiyon DEĞİLDİR. Kabul edilebilir bir KOD HİÇ DEĞİLDİR. >>> Bir kod, bir takım Garantiler Sunmalıdır. Bunlar, >>>> 'Basic Guarantee' : Bir kodun kabul edilebilir bir kod olması için minimum sağlaması gereken şartlar, sunması gereken minimum garantiye 'Basic Guarantee' denmektedir. İş bu garanti kategorisindeki kodlar kaynak sızıntısına mahal vermeyecek, geride 'geçersiz' durumda hiç bir nesne BIRAKMAYACAKTIR. Fakat programda bir durum değişikliği, bir 'state' değişikliği olabilir. Yine benzer şekilde program TUTARLI, yani devam edebilecek, bir durumda kalmalıdır. Örneğin, bizim fonksiyonumuzun bloğunda bir dosyaya yazı yazılıyor olsun. Yazma işleminden sonra da bir nedenden ötürü hata gönderilsin. Programın akışı bizim fonksiyon bloğundan çıkacağı için, varsayalım ki 'Stack-Unwinding' mekanizması ile yazıyı yazan sınıf dosyayı da kapattı, artık programın durumunda bir değişiklik oldu. Yani programın durumu bizim fonksiyonumuz çağrılmadan evvelki durumu ile çağrıldıktan sonraki durumu arasında BİR FARK VARDIR. * Örnek 1, // some codes here... void f(const char* n) { file outF(n, "w"); // 'file' bir sınıf ismi olduğunu varsayalım. if(outF.is_open()) { outF.put("The value is : "); outF.put(g()); // Farz edelim ki 'g()' fonksiyonu bir hata fırlattı. } } // Yukarıdaki senaryoda hata fırlatıldıktan sonra programın akışı 'catch' bloğuna girdiğini varsayalım. // Dolayısıyla arada 'Stack-Unwinding' mekanizması çalışacak ve 'outF' nesnesi için Dtor. çağrılacak. // Bu da ilgili dosyanın kapatılmasını sağlayacak. Fakat artık dosyaya "The Value is : " yazıldığı için // programın durumunda bir değişiklik OLDU. Öte yandan dosya başarı ile kapatıldığı için de kaynak // sızıntısı OLMADI. >>>> 'Strong Guarantee' : Bu garanti türü, 'Basic Guarantee' türünün sağladığı bütün garantileri sunmaktadır. Ek olarak programın durumunda da bir DEĞİŞİKLİK OLMAYACAĞINI GARANTİ ETMEKTEDİR. BİR DİĞER DEYİŞLE BİZİM FONKSİYONUMUZ HİÇ ÇAĞRILMAMIŞ GİBİ OLACAKTIR eğer fonksiyonumuzun bloğundan bir hata gönderilirse. Yani ya işini gör, başarıyla tamamla ya da hiçbir şey yapılmamış durumda bırak. 'Commit or roll-back'. Son olarak program tutarlı, devam edebilecek durumda, OLMALIDIR. * Örnek 1, // some codes here... void f(const char* n) { int temp = g(); // 'g()' fonksiyonunun bir hata fırlatma ihtimaline karşı geri dönüş değerini bir değişkende // sakladık. Böylelikle bir hata göndermesi durumunda programın akışı bu fonksiyondan çıkacağı için // dosyaya da bir şey YAZMADIĞIMIZ İÇİN programımızın durumu da KORUNMUŞ OLDU. file outF(n, "w"); // 'file' bir sınıf ismi olduğunu varsayalım. if(outF.is_open()) { outF.put("The value is : "); outF.put(temp); // Programın akışı buraya geldiyse artık eminiz ki 'g()' fonksiyonumuz bir hata fırlatmadı. } } // İşte yukarıdaki senaryoda ne bir kaynak sızıntısı meydana geldi ne de programın durumunda bir // değişiklik oldu. >>>> 'No-throw Guarantee' : Bu garanti türü ise fonksiyonun işini yapma garantisi vermekte. Fonksiyonun bloğundan bir hata gönderilirse bile kendi yakalayıp işini çözecektir. FONKSİYONDAN DIŞARI HATA GÖNDERİLMEYECEĞİ GARANTİ ALTINDADIR. >>> Bir fonksiyonun exception-safe durumlarını belirtme yolları, >>>> Fonksiyonun bildiriminin sonuna 'noexcept' anahtar sözcüğünü eklemek : Artık iş bu fonksiyonumuz 'No-throw Guarantee' tip bir garanti sunmaktadır. İki tip kullanımı vardır. Eğer çalışma zamanında bu garanti çiğnenir ise 'std::terminate' fonksiyonu çağrılmaktadır. Böylelikle 'std::terminate' fonksiyonunun çağrıldığı dördüncü yer de burası olmuş oldu. Bu anahtar sözcük hem bildirimde hem de tanımda yazılacak. 'Function Overloading' tetiklemez. >>>>> 'Specificaer' olarak kullanımı: * Örnek 1, void func(int)noexcept; // Bu fonksiyon excetion göndermeme garantisi vermektedir. * Örnek 2, void func(int)noexcept(constant_expression); // Derleme zamanında 'constant_expression' olan ifadenin değeri hesaplanır. Eğer, // i. 'true' gelmesi durumunda ilgili ifade " void func(int)noexcept(true); " halini alır ki bu da // exception göndermeme GARANTİSİ VERİYOR, demektir. // ii. 'false' gelmesi durumunda ilgili ifade " void func(int)noexcept(false); " halini alır ki bu da // exception göndermeme GARANTİSİ VERMİYOR, demektir. Yani bir exception gönderilebilir. // # Özetle # // i. void func(int)noexcept; / void func(int)noexcept(true); => Exception GÖNDERMEME GARANTİSİ VAR. // ii. void foo(int); / void foo(int)noexcept(false); => Exception GÖNDERMEME GARANTİSİ YOK. >>>>> Operator olarak kullanılması: Derleme zamanı operatörüdür, tıpkı 'sizeof' gibi tıpkı 'decltype' gibi. Argüman olarak bir FONKSİYON ÇAĞRISI ALMAKTADIR. Eğer iş bu fonksiyon hata gönderme garantisi veriyorsa, ilgili operatörümüz 'true' değer döndürmekte. Aksi halde 'false'. * Örnek 1, #include void func()noexcept {} void foo() {} int main() { /* # OUTPUT # Is func guarantees ? : 1 Is foo guarantees ? : 0 */ constexpr auto isFuncGuarantees = noexcept(func()); constexpr auto isFooGuarantees = noexcept(foo()); std::cout << "Is func guarantees ? : " << isFuncGuarantees << "\n"; std::cout << "Is foo guarantees ? : " << isFooGuarantees << "\n"; } * Örnek 1, her iki kullanım biçiminin ortak potada eritilmesi: //.. int func(); // Exception gönderme ihtimali var. // Eğer 'func' fonksiyonunun exception gönderme garantisi var ise benim de var. Böyle bir garantisi yok // ise benim de yok. int foo()noexcept(noexcept(func())); /* | | | |_ Buradaki kullanım şekli de 'operator' olarak. |̲ Buradaki kullanım şekli 'specifier' olarak. */ void f1(); void f2(); void f3()noexcept( noexcept(f1()) && noexcept(f2()) ); * Örnek 2, Verilen garantinin çiğnenmesi senaryosu, #include void foo() { throw 31; } void func()noexcept; int main() { /* # OUTPUT # terminate called after throwing an instance of 'int' */ func(); } void func()noexcept { foo(); } // Böylelikle 'std::terminate' fonksiyonu dört farklı yerde çağrılabilir; // i. 'Uncaught-exception' durumunda. // ii. Bir exception yakalanması fakat ilgili 'catch' bloğune girmeden önce 'Stack-Unwinding' sırasında // Dtor. fonksiyonunun tekrardan bir exception göndermesi durumunda. // iii. Bir exception fırlatıldığı görülmeden önce derleyicinin 'rethrow' deyimini görmesi durumunda. // iiii. Garanti etmiş olduğu 'exception' tipinin çalışma zamanında çiğnenmesi durumunda. >>>>> 'noexcept' garantisi veren bir fonksiyonun adresini tutan bir 'function pointer' a böyle bir garantiyi vermeyen fonksiyon adresini atayamayız. Fakat tam tersi bir atama geçerlidir. * Örnek 1, //.. void func()noexcept {} void foo(){} int main() { void (*pFunc)()noexcept = func; // Legal. Gerek fonksiyon gerek fonksiyon göstericisi aynı garantiyi sunmakta. void (*pFuncTwo)() = func; // Legal. void (*pFoo)()noexcept = foo; // Sentaks hatası. => error: invalid conversion from // ‘void (*)()’ to ‘void (*)() noexcept’ [-fpermissive] } >> 'Exception Specification' (Depreceated) : Eski kodlarda ilgili fonksiyonun hangi türden hata gönderebileceğini söylemek sistemiydi. ASLA VE ASLA Modern C++ ile KULLANMAMALIYIZ. * Örnek 1, void func(int) throw(std::out_of_range, std::runtime_error); // İlgili 'func' fonksiyonu 'out_of_range' ve 'runtime_error' tip hata gönderebilir. void foo(int) throw(); // İlgili 'foo' fonksiyonu bir hata göndermeyeceğini GARANTİ EDİYOR. // Yukarıdaki fonksiyonlar kenarlarında belirtilen kurala uymadıklarında, yani 'func' fonksiyonu başka türden bir // hata gönderdiğinde veya 'foo' fonksiyonu herhangi türden bir hata gönderdiğinde, derleyici 'std::unexpected' // fonksiyonunu çağırmaktaydı. Bu da 'std::terminate' fonksiyonunu, o ise 'abort()' fonksiyonunu çağırmaktaydı. >> Function Try-Block: Bir fonksiyonun gövdesinden gönderilecek hataları yakalamanın en garantili yolu, o fonksiyonun kodlarını 'try' bloğu içerisine almak ve yeteri kadar 'catch()' blokları ile desteklemektir. * Örnek 1, //.. void foo() { try{ // all function code... } catch(...) { //.. catch codes. } } // Böylelikle ilgili fonksiyonumuzun gönderme ihtimali olan her tür hatayı yakalayabiliriz. Peki ilgili // fonksiyonumuz bir Ctor. fonksiyonu olsaydı ne olacaktı? İkinci örneği inceleyelim. * Örnek 2, #include class Member { public: Member() { std::cout << "Member Ctor.\n"; throw 31; } }; class Myclass{ public: Myclass() : mx() { // Eğer programın akışı buraya gelmişse, ilgili sınıfımızın veri elemanları hayata gelmiş demektir // velev ki onların Ctor. fonksiyonları da bir exception fırlatmadıysa. Eğer onlar da fırlatmış ise // onları burada yakalamamız MÜMKÜN DEĞİLDİR. Olsa olsa nesneyi oluşturduğumuz yerde yakalayabiliriz. try { // some other codes. } catch(...) { std::cout << "Myclass Ctor. içinde hata yakalandi.\n"; } } private: Member mx; }; int main() { /* # OUTPUT # Member Ctor. main işlevi içinde hata yakalandi. */ try{ Myclass myClassX; } catch(...) { std::cout << "main işlevi içinde hata yakalandi.\n"; } // Peki veri elemanlarının göndermiş olduğu hataları, sahibi olduğu sınıfın 'Ctor.' fonksiyonu içerisinde // yakalamanın bir yolu var mıdır? El-cevap : Funciton Try-Block mekanizması işte bu soruya cevaptır. } * Örnek 3, Function Try-Block kullanımı. Normal bir fonksiyon için bu mekanizmayı kullanmanın pek de bir esprisi yoktur. Asıl faydası yukarıdaki probleme çözüm olmaktır. // Genel geçer fonksiyonlarda kullanımı: int func(int q) try{ // Bu alan hem 'func' fonksiyonunun bloğu hem de 'try' bloğu. Dolayısıyla bu blok içerisinde tanımlanan // isimleri, aşağıdaki 'catch' bloğu içerisinde kullanamayız. Sadece fonksiyonun parametresi olan ismi // hem burada hem de 'catch' bloğunda kullanabiliriz. // Hem burada hem de 'catch' bloğu içerisinde 'return' deyimini KULLANABİLİRİZ. } catch(int x) { } // Sınıfların Ctor. fonksiyonları için kullanımı: #include class Member { public: Member() { std::cout << "Member Ctor.\n"; throw 31; } }; class Myclass{ public: Myclass() try : mx() { /* // Artık blok içerisindeki bu ikinci kısım lüzumsuz hale geldi. try { // some other codes. } catch(...) { std::cout << "Myclass Ctor. içinde hata yakalandi.\n"; } */ } catch(...) { std::cout << "Myclass Function Try-Block içinde hata yakalandi.\n"; // Artık burada bizler ya programı sonlandırmalıyız ya ya ilgili hatayı 'translate' etmeliyiz ya // da hatayı 're-throw' etmeliyiz. Eğer hiç bir şey yapmadan bırakırsak, derleyici aynı hatayı // 're-throw' EDİYOR. } private: Member mx; }; int main() { /* # OUTPUT # Member Ctor. Myclass Function Try-Block içinde hata yakalandi. main işlevi içinde hata yakalandi. */ try{ Myclass myClassX; } catch(...) { std::cout << "main işlevi içinde hata yakalandi.\n"; } // Çıktıdan da görüldüğü üzere ilgili Ctor. fonksiyonuna ait Function-Try Block içerisinde yakalanan // hataya ellemediğimiz için derleyici tarafından 're-throw' edilmektedir. } * Örnek 4, //.. class Myclass{ public: explicit Myclass() try : m_x{31} { std::cout << "Myclass::Myclass(int " << m_x << ") was called." << std::endl; throw 62; } catch(int) { std::cout << "An exception has been caught!!!" << std::endl; } friend std::ostream& operator<<(std::ostream& os, const Myclass& other) { return os << "[" << other.m_x << "]\n"; } private: int m_x{}; }; class Other{ public: inline static Myclass myClass{}; }; int main() { /* # OUTPUT # Myclass::Myclass(int 31) was called. An exception has been caught!!! terminate called after throwing an instance of 'int' */ return 0; } >> Statik ömürlü nesnelerin hayata gelmesi sırasında DIŞARIYA bir hata gönderilirse BU HATAYI HİÇ BİR ŞEKİLDE YAKALAYAMAYIZ. Bir diğer deyişle global isim alanındaki nesnelerin veya sınıfların 'static' veri elemanı olan nesnelerin Ctor. fonksiyonlarından dışarıya fırlatılan hataları YAKALAYAMAYIZ. Çünkü bu tip nesneler 'main' fonksiyonundan da önce çağrılmaktadırlar. Sadece 'function try-block' kullanarak yakalama yapabiliriz.