C++ Hata Düzeneği Güvenliği (exception safety) Ali Çehreli acehreli@yahoo.com Nesne yaşam süreçleri Hatalar (exceptions) RAII yöntemi Akıllı göstergeler (smart pointers) Hata güvenliği (exception safety) 1
Kaynaklar Herb Sutter'ın Guru of the Week bilmeceleri (http://gotw.ca/gotw/) Herb Sutter'ın GotW'lerin genişletilmiş hali olan kitabı Exceptional C++ (Türkçesi: "Sıradışı C++") boost.org akıllı göstergeleri Andrei Alexandrescu'nun Loki kütüphanesinin davranışı ayarlanabilen (policy based) akıllı göstergeleri 2
Nesne türleri ve yaşam süreçleri Otomatik nesne: Yaşam süreci derleyici tarafından yönetilir; yerel nesneler, parametreler, sınıf ve yapı üyeleri, geçici nesneler Dinamik nesne: Yaşam süreci programcı tarafından belirlenir; new ile başlar, delete ile biter Statik nesne: Yaşam süreci derleyici tarafından yönetilir; bunlar konumuzun dışında 3
Otomatik ve dinamik nesneler void foo() A a0; D * d = new D; // dinamik A a1; // < a1 burada sonlanır // < a0 burada sonlanır 4
Nesne kurulum adımları 1) Sanal (virtual) üst sınıfların kurulmaları 2) Diğer üst sınıfların kurulmaları 3) Üyelerin tanımlandıkları sırada ilklenmeleri 4) Kurucu işlevin (constructor) işletilmesi 5
Nesne sonlanma adımları 1) Sonlandırıcı işlevin (destructor) işletilmesi 2) Üyelerin ve üst sınıfların kuruldukları sıranın tersinde sonlandırılmaları Temel türlerin sonlandırıcıları "boştur". Örneğin int'ler özel bir son değer almazlar, göstergeler (işaretçi (pointer)) gösterdikleri nesneleri sonlandırmazlar, vs. 6
Kurulum başarısız olduğunda... 1) O ana kadar kurulmuş olan nesneler ters sırada sonlandırılırlar 2) Eğer nesne için new ile bellekten yer ayrılmışsa, o yer geri verilir Kurulmakta olan nesnenin sonlandırıcı işlevi çağrılmaz 7
Nesne kabul edilebilmek için tam olarak kurulmuş olmak gerekir class BirSinif : /* ust siniflar */ /*... */ BirSinif(/*... */) : /* ust siniflarin ve uyelerin ilklenmeleri */ /* kurulumun diger adimlari */ ; // < ancak simdi "nesne"
Hata düzeneğinin işleyişi Bir iş yapmaya çalışılırken ( try edilirken ) hata atılabilir ( throw edilebilir ), ve hata yakalanabilir ( catch edilebilir ).
Bir try/catch örneği void bir_islev() try bir_is_yap(); isletilmesi_garanti_degil(); catch (const BirHata & hata) /* hata durumu islemleri */ void isletilmesi_garanti_degil() if (bir_kosul) throw BirHata(); bunun_isletilmesi_de_garanti_degil(); 10
Program yığıtının temizlenmesi (stack unwinding) Hata yakalanana kadar çıkılan bütün kapsamlardaki bütün nesneler otomatik olarak sonlandırılırlar.
Ancak tek hata atılı olabilir Program yığıtının atılmış olan bir hata nedeniyle temizlenmesi sırasında yeni bir hata atılırsa program abort ile hemen sonlanır. İlke: Sonlandırıcı işlevlerden hata sızmasına izin vermeyin. 12
C'de güvenli kaynak yönetimi int bir_islev(kaynak ** sonuc) int hata = 0; cikis: Kaynak * k0 = NULL; Kaynak * k1 = NULL; hata = kaynak_ayir(&k0); if (hata) goto cikis; hata = kaynak_ayir(&k1); if (hata) goto cikis; /* r0 ve r1 burada kullaniliyor olsun */ if (hata) goto cikis; /* sahipligi cagirana gecir */ *sonuc = k0; k0 = NULL; kaynak_geri_ver(&k1); kaynak_geri_ver(&k0); return hata; 13
C'de güvenli kaynak yönetimi int bir_islev(kaynak ** sonuc) int hata = 0; cikis: Kaynak * k0 = NULL; Kaynak * k1 = NULL; hata = kaynak_ayir(&k0); if (hata) goto cikis; hata = kaynak_ayir(&k1); if (hata) goto cikis; /* k0 ve k1 burada kullaniliyor olsun */ if (hata) goto cikis; /* sahipligi cagirana gecir */ *sonuc = k0; k0 = NULL; kaynak_geri_ver(&k1); kaynak_geri_ver(&k0); return hata; 14
C'de güvenli kaynak yönetimi int bir_islev(kaynak ** sonuc) int hata = 0; cikis: Kaynak * k0 = NULL; Kaynak * k1 = NULL; hata = kaynak_ayir(&k0); if (hata) goto cikis; hata = kaynak_ayir(&k1); if (hata) goto cikis; /* k0 ve k1 burada kullaniliyor olsun */ if (hata) goto cikis; /* sahipligi cagirana gecir */ *sonuc = k0; k0 = NULL; kaynak_geri_ver(&k1); kaynak_geri_ver(&k0); return hata; 15
C++'da güvenli kaynak yönetimi Kaynak bir_islev() Kaynak k0(/*... */); Kaynak k1(/*... */); /* k0 ve k1 burada kullaniliyor olsun */ /* sahipligi cagirana gecir */ return k0; 16
Hata ne zaman atılmalı? a) her hatada mı? b) önemli hatalarda mı? c) iş doğru olarak yapılamadığında mı?
Hata ne zaman yakalanmalı? Çok nadiren ve ancak o hata karşısında yapacak bir şey varsa
Kaynağın açıkça geri verilmesi... void foo() Kaynak * k = ayir(); /*... */ geri_ver(k); // < acikca 19
... C++'da işe yaramaz void foo() Kaynak * k = ayir(); /*... */ // < hata atilabilir geri_ver(k); // < isletilmeyebilir 20
RAII yöntemi (Resource Acquisition Is Initialization) geri verme işlemi sonlanmadır geri verme işlemi sonlandırıcıya ait olmalıdır İlke: Kaynak kod içinde değil, o kaynaktan sorumlu olan sınıfın sonlandırıcı işlevinde geri verilmelidir 21
RAII örneği void foo_umutlu() Kaynak * k = ayir(); /*... */ // < hata atilabilir geri_ver(k); // < isletilmeyebilir void foo_raii() KaynakSorumlusu k(ayir()); /*... */ // < hata atilabilir; sorun yok 22
Akıllı göstergeler Sorumlusu oldukları nesneleri delete ile sonlandıran RAII nesneleridir Sıradan göstergeler kadar rahat kullanılırlar Daha akıllı da olabilirler 23
Bir akıllı gösterge gerçekleştirmesi class AkilliGosterge BirTur * ptr; AkilliGosterge g(new BirTur()); public: explicit AkilliGosterge(BirTur * p = 0) : ptr(p) ~AkilliGosterge() delete ptr; ; 24
Bir akıllı gösterge gerçekleştirmesi class AkilliGosterge BirTur * ptr; AkilliGosterge g(new BirTur()); foo(g.get()); public: explicit AkilliGosterge(BirTur * p = 0) : ptr(p) ~AkilliGosterge() delete ptr; BirTur * get() const return ptr; ; 25
Bir akıllı gösterge gerçekleştirmesi class AkilliGosterge BirTur * ptr; AkilliGosterge g(new BirTur()); foo(g.get()); public: explicit AkilliGosterge(BirTur * p = 0) : ptr(p) BirTur & r = *g; r.i = 7; ~AkilliGosterge() delete ptr; BirTur * get() const return ptr; BirTur & operator* () const return *ptr; ; 26
Bir akıllı gösterge gerçekleştirmesi class AkilliGosterge BirTur * ptr; public: AkilliGosterge g(new BirTur()); foo(g.get()); BirTur & r = *g; r.i = 7; explicit AkilliGosterge(BirTur * p = 0) : ptr(p) g >i = 42; ~AkilliGosterge() delete ptr; BirTur * get() const return ptr; BirTur & operator* () const return *ptr; BirTur * operator >() const return ptr; ; 27
Bir akıllı gösterge gerçekleştirmesi class AkilliGosterge BirTur * ptr; public: AkilliGosterge g(new BirTur()); foo(g.get()); BirTur & r = *g; r.i = 7; explicit AkilliGosterge(BirTur * p = 0) : ptr(p) ~AkilliGosterge() delete ptr; g >i = 42; if (!g) /*... */ BirTur * get() const return ptr; BirTur & operator* () const return *ptr; BirTur * operator >() const return ptr; bool operator! () const return ptr == 0; ; 28
class AkilliGosterge BirTur * ptr; public: Bir akıllı gösterge gerçekleştirmesi AkilliGosterge g(new BirTur()); foo(g.get()); BirTur & r = *g; r.i = 7; explicit AkilliGosterge(BirTur * p = 0) : ptr(p) ~AkilliGosterge() delete ptr; g >i = 42; if (!g) /*... */ AkilliGosterge g1(g); // HATA g2 = g; // HATA BirTur * get() const return ptr; BirTur & operator* () const return *ptr; BirTur * operator >() const return ptr; bool operator! () const return ptr == 0; private: ; AkilliGosterge(AkilliGosterge const &); AkilliGosterge & operator= (AkilliGosterge const &); 29
boost::scoped_ptr #include <boost/scoped_ptr.hpp> typedef boost::scoped_ptr<hayvan> HayvanPtr; void foo() HayvanPtr hayvan(new Kedi); hayvan >oyna(); /*... */ // Kedi burada sonlanir 30
boost::shared_ptr (paylaşımlı sahiplik) 31
boost::weak_ptr (gözlemci) 32
Nesne gider, boost::weak_ptr kalır 33
boost::intrusive_ptr (daha hızlı paylaşımlı sahiplik) Türün daha hızlı bir sayma düzeneği bulunmalı Harcanan bellek yalnızca bir T* kadardır İki tane yardımcı işlev tanımlanarak kullanılır: void intrusive_ptr_add_ref(t *); void intrusive_ptr_release(t *); 34
boost::scoped_array boost::scoped_ptr gibidir ama delete[]'i çağırır Onun yerine std::vector<birakilligosterge> de düşünülebilir 35
boost::shared_array boost::shared_ptr gibidir ama delete[]'i çağırır Onun yerine std::vector<birakilligosterge> de düşünülebilir 36
std::auto_ptr (aslında std::transfer_ptr da denebilirmiş) Nesne belirli bir anda en fazla tek auto_ptr tarafından sahiplenilir auto_ptr kopyalandığında veya atandığında sahiplik el değiştirir: hedef yeni sahip haline gelir, kaynak "NULL olur" standart topluluklarla (örneğin vector) kullanılamaz 37
C++ Technical Report 1, Boost'taki akıllı göstergelerin bazılarını standart kütüphaneye ekledi std::tr1::shared_ptr std::tr1::weak_ptr başka? 38
boost::ptr_vector Barındırdığı göstergelerin eriştirdiği nesnelere de sahiptir vector içindeki göstergeler için teker teker delete'i çağırır 39
Hata güvenliğinde iki önemli tarih 1994: Tom Cargill'in "Exception Handling: A False Sense of Security" makalesinde C++ camiasını uyarması: "Atılabilecek hatalar karşısında tam güvenli olarak işleyen bir Stack şablonu yazılamaz" 1997: İlk doğru çözümün Herb Sutter'ın comp.lang.c++.moderated haber grubunda başlattığı Guru of the Week'lerin 8 numaralı konusunda çıkması 40
Tom Cargill'in iddiası: T'nin kopyalayıcısının hata atma olasılığı varsa, Stack::pop() tam güvenli olarak yazılamaz template <class T> class Stack int adet_; T * elemanlar_; public: /*... */ /* Elemani en tepeye ekler */ void push(const T &); /* En tepedekini dondurur */ T pop() /*... */ ; T tepedeki = elemanlar_[adet_ 1]; adet_; return tepedeki; 41
template <class T> class Stack int adet_; T * elemanlar_; public: /*... */ void push(const T &); Güvenli bir Stack arayüzü /* Tepedekine erisim saglar */ T & top() return stack_[adet_ 1]; ; void pop() adet_; İlke: Arayüzleri hatalara karşı güvenli olacak şekilde tasarlayın 42
Üç işlevler kuralı Eğer sonlandırıcı (destructor) kopyalayıcı (copy constructor) atama işleci (operator=) üçlüsünden birisini tanımlamanız gerekmişse, hemen hemen her durumda diğerlerini de en azından tanımsız olarak bildirmeniz gerekir. 43
Görünürde güvenlik Bu sınıf güvenli midir? class UmutluSorumlu : private boost::noncopyable Bir * bir_; Iki * iki_; public: UmutluSorumlu() : bir_(new Bir()), iki_(new Iki()) /* kurulumun geri kalani */ ; ~UmutluSorumlu() delete iki_; delete bir_; 44
Görünürde güvenlik Güvenli değil! class UmutluSorumlu : private boost::noncopyable Bir * bir_; Iki * iki_; public: UmutluSorumlu() : bir_(new Bir()), iki_(new Iki()) < 1: new hata atabilir, < 2: Iki() hata atabilir /* kurulumun geri kalani */ < 3: baska hata atilabilir ; ~UmutluSorumlu() delete iki_; delete bir_; İlke: Tek sahibin tek nesnesi olsun 45
Şimdi her sahibin tek nesnesi var Bu sefer güvenli mi? class SupheliSorumlu BirSorumlusu bir_; IkiSorumlusu iki_; // Bu nesnelerin kopyalama ve atama islemlerinin // guvenli olduklarini varsayalim // (ornegin std::string) public: SupheliSorumlu() : bir_(new Bir()), iki_(new Iki()) /* kurulumun geri kalani */ // Sonlandirici isleve artik gerek yok... ; 46
Eğer üyelerin atama işlemleri "normal" ise, bu sınıf güvenli değildir. class SupheliSorumlu std::string bir_; std::string iki_; public: SupheliSorumlu() : bir_( bir ), iki_( iki ) /* kurulumun geri kalani */ // Sonlandirici isleve artik gerek yok... // Bu sinif, yarim atanmis durumda kalabilir: iki_'nin atanma islemi // sirasinda hata atilirsa; bir_ degismistir, iki_ eski degerinde kalir ; 47
Tek işlev kuralı Eğer sınıfın birden fazla üyesi varsa, atama işleci (operator=) hemen hemen her durumda en azından tanımsız olarak bildirilmelidir. 48
operator= işlecinin bozuk tanımı class Sinif /*... */ Sinif & operator= (const Sinif & sagdaki) if (this!= &sagdaki) // 1) bu nesneyi sonlandir // 2) sagdakinden kopyala < hata atabilir! ; return *this; 49
operator= işlecinin modern tanımı class GuvenliSorumlu BirPtr bir_; IkiPtr iki_; string uc_; /*... */ GuvenliSorumlu & operator= (const GuvenliSorumlu & sagdaki) GuvenliSorumlu gecici(sagdaki); // 1) Once kopyalamayi dene this >swap(temp); // 2) Sonra degistir return *this; // < Eski durum bu noktada sonlanir ; // Degis tokus eder; Hata atmaz! void swap(guvenlisorumlu & sagdaki) bir_.swap(sagdaki.bir_); iki_.swap(sagdaki.iki_); uc_.swap(sagdaki.uc_); 50
Hata güvenliği garantileri Temel garanti: Kaynak sızıntısı yok ve nesneler kullanılabilir (tutarlı ama kestirilemez) durumdalar Tam garanti: Programın durumunda hiçbir değişiklik yok Hata sızdırmama garantisi: Ne kendisi atar, ne çağırdığı işlevler atar (örneğin std::swap) 51
Özet: Hata güvenliği ilkeleri Sonlandırıcı işlevlerden hata sızdırmayın Kaynakları kod içinde açıkça geri vermeyin (RAII) Tek sahibin tek nesnesi olsun Hata atabilecek işleri önceye alın; değişikleri ondan sonra yapın Hata güvenliği sonraya bırakılamaz Bir çok işi birden yapmayın (top() ve pop() gibi) 52
Hatırlatma: Akıllı göstergeler boost::scoped_ptr: basit kaynak sorumlusu; kopyalanamaz boost::shared_ptr: paylaşımlı sahiplik (reference counted) boost::weak_ptr: sahipliğe karışmaz; shared_ptr gözlemcisidir std::auto_ptr: sahipliği devreder vs... 53