Tam Sürümünü Görmek İçin : Aykiri durumlarin C++'in C'den farkliligina etkisi
acehreli
11/11/2004, 02:08
Bu konuyu "Fark ve Baglilik" adli konudaki sohbetin devami olarak aciyorum. Orada asil konudan fazlaca ayrildigimiz icin yeni bir konunun daha iyi olacagini dusunduk.
Aykiri durum (exception), kodun normal akisi disindaki durumlara denir. C++'ta aykiri durumlar programin isleyisini sonlandiran durumlar degillerdir. Programin bir aykiri durum sonucu sonlanmasi, ancak o aykiri durumun yakalanmamasi durumunda olusur.
Burada aykiri durumlarin ayrintilarina girmek istemiyorum. Ilgilenenler iyi C++ kitaplarindan ve ozellikle Herb Sutter'in yazilarindan ve kitaplarindan yararlanabilirler.
C++'in aykiri durumlar olmadan dogru olarak kullanilamayacaginin bir ornegini vermekle yetinecegim: Kurucu islevlerin (constructor) donus degerleri olmadigi icin, o islevlerin nesneyi olusturamamasi durumunda yapilacak sey aykiri durum atmaktir. Boyle bir durumda program sonlanmaz, aykiri durumun yakalandigi noktadan islemeye devam eder.
[Not: Bazi programcilar kurucu islevlerin basit ilk deger atama islemleri yapmasi gerektigini, nesnenin asil olusturulmasinin adi geleneksel olarak 'init' olan bir isleve birakilmasini savunurlar. Bu, C++ programlarinda geriye atilmis bir adimdir. Ayrica her nesneyi kurduktan sonra nesne.init() demenin unutulmamasini gerektirdigi icin, riskli bir yontemdir. Ornegin ben programlarimi bu sekilde yazmam; nesne olusamiyorsa bu durumu bildiren bir aykiri durum atarim.]
Hemen aklima gelen baska bir ornek, 'new'un de aykiri durum atiyor olmasidir. C programcilarinin
int * p;
if ((p = malloc(/* ... */)) == 0)
olarak cok tanidiklari program yapisi, C++'ta soyle yazilir:
int * p = new /* ... */;
C++'ta new 0 dondurmedigi icin, p'nin degeri denetlenmez. Bellek ayrilamamissa std::bad_alloc atilmistir. O aykiri durumla ilgilenen ust duzey islev de kullaniciya ornegin "o kadar cok sayida nesne olusturamiyorum; lutfen daha kucuk bir sayi girin" gibi bir sey soyler ve program isleyisine devam eder.
C gibi yazilmis C++ programlari boyle bir durumda kaynak kaybetme riskiyle karsi karsiyadirlar: kod satirlari icine yazilmis olan delete, unlock, vs. gibi satirlar isletilememis olurlar:
void foo(size_t adet)
{
kilitle();
cok_sayida_nesne_olustur(adet);
kilidi_ac();
}
adet cok buyuk bir sayiysa ve bu yuzden cok_sayida_nesne_olustur islevi bellek ayiramiyorsa, kilidi_ac() cagrisi yapilmayacaktir. Program kilitlenip kalma tehlikesi altindadir.
Soylemeye calistigim seyin ozu de bu iste: C programcisi yukaridaki foo gibi bir islev yazar, C++ programcisi ise asagidaki foo gibi:
void foo(size_t adet)
{
Kilit kilit;
cok_sayida_nesne_olustur(adet);
}
Bu iki islevin yazilis sekli ve buna goturen dusunce tarzi C ve C++'i birbirlerinden ayri diller yapacak kadar farklidir.
Iste onceki mektubumda verdigim Yigit::cikart islevi, islevin dondurdugu T'nin kopyalanmasi sirasinda atilmasi olasi ve Yigit::cikart ile tamamen ilgisiz bir aykiri durum nedeniyle yanlis calismis olur. (Tepedeki nesne, cagiranin ortamina kopyalanamadigi halde yigittan cikartilmis ve kaybedilmistir.)
Euclides, bunun "sadece 1 madde" oldugunu soyluyorsun. Ben de "bes alti" madde icinden sectigim bir tanesinin bile C++'i C'den baska bir dil olarak kabul edilebilecek kadar degistirdigini soyluyorum. Eger bunda basarili olursam, baska madde uzerinde konusmama gerek kalmaz.
Kernighan & Ritchie'nin The C Programming Language'i uc yuz sayfanin altindadir. Stroustrup'un C++'i tanimladigi The C++ Programming Language'i ise bin sayfadan fazladir. Stroustrup'un kitabinin sayfalarindan rastgele bir tanesini acarsak, C'de olmayan olanaklardan birisine rastlama sansimiz yuksektir.
Ali
acehreli
11/11/2004, 02:13
Soylemeyi unuttum: Herb Sutter, aykiri durumlarin hangi durumlarda kullanilmalari gerektigini C/C++ Users Journal'in Agustos 2004 sayisinda cikan When and How to Use Exceptions adli yazisinda irdeler.
Ali
Volkan Uzun
11/11/2004, 02:25
peki bellegi free edememek gibi durumla karsilasabilirmiyiz ?
bu durumda destructorda exception atilirmi ?
acehreli
11/11/2004, 02:45
Destructor'larda (bozucu islev) aykiri durum atilmasi 'evil' (seytani) olarak tanimlanir. Bozucu islevlerden aykiri durum sizmamasi temel bir C++ kodlama ogudu olarak gecer.
Boyle bir durum benim basima daha once geldi aslinda. Bozucu islevin RPC (remote procedure call) uzerinden bir cagri yapmasi gerekiyordu. Bozucunun bir aykiri durum yuzunden cagrilmis oldugu bir durumda RPC kutuphanesinin de ayrica bir aykiri durum atmasi yuzunden program gocuyordu.
Sanirim bozucularin hatasiz calisan islevler olmalari dusunuluyor. "Bozucudan aykiri durum sizdirmayin" ogudu disinda bu konuda daha fazla deneyimim yok. Herhalde hatayi kaydetmek disinda pek bir sey yapilamaz. (?)
Ali
Euclides
13/11/2004, 12:20
@acehreli :
3 mesajdır aynı lafı tekrarlayıp duruyorsun ama hala bir örnek kod göremedik
Aykiri durum (exception), kodun normal akisi disindaki durumlara denir. C++'ta aykiri durumlar programin isleyisini sonlandiran durumlar degillerdir. Programin bir aykiri durum sonucu sonlanmasi, ancak o aykiri durumun yakalanmamasi durumunda olusur.
anlaşılan benim senin mesajlarını okuduğum gibi sen benimki leri okumuyorsun....
acehreli
15/11/2004, 08:37
Anlatmaya calistigimi tamamlamak icin bir ornek kod veriyorum. Bu kod, herhangi bir uzak sistemle iletisim kuran bir programi temsil ediyor. Linux ortaminda yazip denedim; ama yalnizca standart kutuphaneyi ve bir tek POSIX islevi (nanosleep) kullandigim icin cogu ortamda calisacagini dusunuyorum.
Programi calistirmak icin ya 'hatali' secenegini, ya da 'dogru' secenegini verin. Ornegin:
deneme hatali
veya
deneme dogru
Uzak sistemi temsil etmek icin adi "iletisim_kutugu" olan bir kutuk dusundum. Program, bu kutugu calistirildigi dizin icinde olusturuyor ve isleyisine mutlu mutlu devam ediyor.
Program calismaya basladiktan sonra biz disaridan iki sey yapabiliyoruz. (Bunlari yapabilmek icin ikinci bir komut satiri penceresine ihtiyaciniz olabilir. Ben oyle yaptim.):
1) Programi sonlandirmak icin bu kutugun icine, ilk sozcuk olarak "yeter" yazdiriyoruz. Bu durumda program kaynak kaybedip kaybetmedigini denetledikten sonra kapaniyor. Bunu Linux'ta yapmak icin su kabuk komutunu kullanabilirsiniz:
echo yeter > iletisim_kutugu
Windows altinda da ya benzer sekilde, ya da icinde 'yeter' sozcugu bulunan baska bir kutugu iletisim_kutugu'nun ustune kopyalayarak yapabilirsiniz (Ben bunu denemedim.):
copy iletisim_kutugu_yeter iletisim_kutugu
2) Programin uzak sistemle iletisiminin koptugunu temsil etmek icin kutugu silebilirsiniz. Linux altinda:
rm iletisim_kutugu
Windows altinda da
del iletisim_kutugu
Boyle yapinca, program tekrar iletisim kurmayi deniyor ve isine kaldigi yerden devam ediyor. (Yani iletisim_kutugu adli kutugu tekrar olusturuyor.)
Hatayi gormek icin su adimlari izleyebilirsiniz:
1) Programi 'hatali' secenegi ile baslatin: deneme hatali
2) Bir kac saniye bekleyin
3) Iletisimi kopartin: rm iletisim_kutugu
4) Bir kac saniye bekleyin (iletisim tekrar kurulacaktir)
5) Programi sonlandirin: echo yeter > iletisim_kutugu
Program kendisini denetleyecek ve cikisa "HATA YAPMISIM!" yazacak...
Bundan sonra, programi bir de 'dogru' secenegini kullanacak sekilde calistirin. Yani birinci adimda komut satirinda 'deneme dogru' yazin. 2-4 adimlarini islettiginizde programin kaynak kaybetmedigini goreceksiniz: Bu sefer cikisina "Dogru calismisim" yazacak.
Program kodu asagidaki gibi... Program, 'hatali' secildiginde bir_is_yap_hatali islevini, 'dogru' secildiginde ise bir_is_yap_dogru islevini kullaniyor. Butun kodu incelemek yerine yalnizca bu iki islevi karsilastirabilirsiniz. Boyle yaptiginizda, aslinda yapi olarak daha onceden gosterdigim C gibi ve C++ gibi yazilan foo islevlerinin aynilari olduklarini goreceksiniz.
#include <iostream>
#include <string>
#include <fstream>
#include <set>
#include <iterator>
#include <time.h>
#include <stdlib.h>
using namespace std;
// Programi sonlandirmak icin kullanilan sozcuk
string const sonlandirma_sozcugu = "yeter";
// Programin devam etmesi icin kullanilan sozcuk
string const devam_etme_sozcugu = "devam";
// Iletisim ortami olarak kullanilan kutugun adi
string const iletisim_kutugu_adi = "iletisim_kutugu";
// Kutukten okuma yapmadan once bu kadar saniye bekleyecegiz
int const iletisim_arasi = 2;
// Iletisim koptugunda tekrar baglanmadan once bu kadar bekleyecegiz
int const tekrar_baglanma_suresi = 3;
// Hayali bir kaynak
typedef int Kilit;
// Programin kullandigi butun kaynaklari tutan bir topluluk
set<Kilit> kilitler;
// Gercek bir kaynak kullanmak yerine, rastgele bir sayi
// saklayacagiz. Bir anlamda, ele gecirdigimiz bir kilidi
// bir sayi ile gosteriyor gibi olacagiz.
Kilit kilitle()
{
Kilit const kilit = rand();
cout << "kilitledim: " << kilit << '\n';
kilitler.insert(kilit);
return kilit;
}
// Hayali kilit acma isini de daha once sakladigimiz sayiyi
// topluluktan cikartarak gerceklestirecegiz
void kilidi_ac(Kilit kilit)
{
kilitler.erase(kilit);
cout << "kilidi actim: " << kilit << '\n';
}
// Bir iletisim hatasi oldugunda atilacak olan aykiri durum.
// Normalde bu sinifin hata ile ilgili bilgi vermesi de
// beklenir ama biz burada yalnizca hatanin turu ile
// ilgilenecegimiz icin, baska bir bilgi
// tutmayacagiz. Ayrica, butun hatalarin dogrudan veya
// dolayli olarak std::exception'dan turemeleri
// onerilir. Burada bu oneriyi de gozardi ediyoruz.
class IletisimHatasi
{};
// Iletisim ortamindan bir sozcuk okur ve ekrana
// yazdirir. Eger okudugu sozcuk sonlandirma_sozcugu'ne
// esitse 'true', degilse 'false' dondurur.
bool ara_isler_yap(string const & kutuk_adi)
{
ifstream kutuk(kutuk_adi.c_str());
if (!kutuk)
{
// Isimizi yapabilmemiz icin gereken kutukte bir
// sorun var. Devam edemeyiz...
throw IletisimHatasi();
}
string sozcuk;
kutuk >> sozcuk;
cout << "okudum: " << sozcuk << '\n';
return (sozcuk == sonlandirma_sozcugu);
}
// Yaptigi asil ise gecmeden once bir kaynak ayirmasi
// gereken bir islev. C mantigi ile yazildigi icin, kaynagi
// acikca kod icinde geriye veriyor. Bu, aykiri durumlar
// atilmasi durumunda kaynak kaybina neden olacaktir. Bu
// islev, isin sonlanmasi gereken durumda 'true' dondurur.
bool bir_is_yap_hatali(string const & kutuk_adi)
{
Kilit kilit = kilitle();
bool const yeter = ara_isler_yap(kutuk_adi);
kilidi_ac(kilit);
return yeter;
}
// Kilit acmakla (geri vermekle) gorevli bir sinif. Yaptigi
// tek is, kendisine emanet edilen kilidi kendisi
// sonlanirken acmaktir.
class KilitAcici
{
Kilit kilit_;
KilitAcici(KilitAcici const &);
KilitAcici & operator= (KilitAcici const &);
public:
explicit KilitAcici(Kilit kilit)
:
kilit_(kilit)
{}
~KilitAcici()
{
kilidi_ac(kilit_);
}
};
// Bu islev; yaptigi asil ise gecmeden once, ayirmasi
// gereken kaynagi bir KilitAcici nesnesine emanet
// ediyor. Boylece ne kaynagi acikca geri vermesi gerekiyor,
// ne de aykiri durumlar atildiginda kaynak kaybediyor. Bu
// islev, isin sonlanmasi gereken durumda 'true' dondurur.
bool bir_is_yap_dogru(string const & kutuk_adi)
{
KilitAcici const kilit_acici(kilitle());
return ara_isler_yap(kutuk_adi);
}
// Iletisim kurma isini programin calistigi dizinde bir
// kutuk olusturarak temsil ediyoruz.
void iletisim_kur(string const & kutuk_adi)
{
ofstream kutuk(kutuk_adi.c_str());
kutuk << devam_etme_sozcugu << '\n';
cout << "iletisim kuruldu: " << kutuk_adi << '\n';
}
// Programi bekleten bir islev
void bekle(int saniye)
{
cout << saniye << " saniye bekliyorum\n";
timespec const sure = { saniye, 0 };
nanosleep(&sure, 0);
}
// Komut satirinda kullanilabilecek program secenekleri
string const program_secenegi_hatali = "hatali";
string const program_secenegi_dogru = "dogru";
// Programin nasil kullanildigini gosteren islev
void kullanim_goster(string const & program_adi)
{
cerr << "Kullanim: " << program_adi << " {"
<< program_secenegi_hatali << '|'
<< program_secenegi_dogru << "}\n";
}
// bir_is_yap_hatali ve bir_is_yap_dogru islevlerini
// gostermek icin kullanilacak olan islev gostergesi turu.
typedef bool (*Islev)(string const &);
// Programin calistirildigi islev. Duruma gore,
// bir_is_yap_hatali veya bir_is_yap_dogru islevini cagirir.
void calistir(Islev islev)
{
string const kutuk_adi = iletisim_kutugu_adi;
iletisim_kur(kutuk_adi);
bool yeter = false;
while (!yeter)
{
try
{
bekle(iletisim_arasi);
yeter = islev(kutuk_adi);
}
catch (IletisimHatasi const & hata)
{
cout << "ILETISIM HATASI!\n";
cout << "tekrar iletisim kurmayi deniyorum\n";
bekle(tekrar_baglanma_suresi);
iletisim_kur(kutuk_adi);
}
}
cout << "yetti\n\n";
}
// Programin kaynak kaybedip kaybetmedigini kendi kendisine
// denetledigi islev.
void kendini_denetle()
{
if (kilitler.empty())
{
cout << "Dogru calismisim...\n";
}
else
{
cout << "HATA YAPMISIM! Acilmayan kilit kalmis: ";
copy(kilitler.begin(),
kilitler.end(),
ostream_iterator<Kilit>(cout, " "));
cout << '\n';
}
}
// Komut satirini okuyarak programin nasil calisacagini
// belirleyen islev. Komut satiri hatali oldugunda 0
// dondurur.
Islev programi_hazirla(int argc, char * argv[])
{
Islev islev = 0;
if (argc == 2)
{
string const secenek = argv[1];
if (secenek == program_secenegi_hatali)
{
islev = bir_is_yap_hatali;
}
else if (secenek == program_secenegi_dogru)
{
islev = bir_is_yap_dogru;
}
}
return islev;
}
int main(int argc, char * argv[])
{
Islev const islev = programi_hazirla(argc, argv);
if (!islev)
{
kullanim_goster(argv[0]);
return EXIT_FAILURE;
}
calistir(islev);
kendini_denetle();
}
Ali
for(unsigned int i_=0;i_ < 1000000; ++i_)
{
/*.............*/
y[n] = ........./x[n-1]
/*.............*/
}
boyle cok fazla tekrarlanan bir fonksiyon icin y[n-1] = 0 olmasi durumunda programin sonlanmamasi icin istisna firlatmanin,
if( 0 == x[n-1] )
{
// biseyler yap
}
gibi basit bir kontrol islemine karsi ne gibi getirileri olur ?
tesekkur ederim...
acehreli
19/11/2004, 01:59
C++ aykiri durumlariyla donanim aykiri durumlari ayri seylerdir. Sifirla bolme hatasi C++'in catch ifadesiyle yakalanamaz.
C++'ta aykiri durum atilabilmesi icin bir kodun isletilmesi gerekir:
if (bir_sey)
{
throw Hata();
}
Donanim aykiri durumlari ise, yazmaclardan birisine yasal olmayan bir deger yazilmasiyla bile olusabilir. Sifirla bolme hatasi da boyle bir donanim hatasidir.
Onun icin, donanim hatalarini bir kenara birakacagim. Soru soyle olsun: Hatali durumlari bastan kendimiz denetlemek yerine, nasilsa ic islevlerden atilacak olan hatalara mi guvenmeliyiz? Boyle yaparsak bedeli ne olur?
Aykiri durumlar yararlidir:
- hata denetimini kodun normal akisi disina cikartirlar
- program hatali durumlari gozden kaciramaz; gozden kacan aykiri durum oldugunda program sonlanir
- hatalar her turden olabilir ve her turden bilgi tasiyabilirler
- (baska?)
Peki aykiri durumlar bedel getirirler mi? Bunu yanitlamanin tek bir yolu vardir: iki turlu yazmak ve program hizlarini karsilastirmak. Yine de, aykiri durum kullaniminin programa bir bedel ekledigini soyleyebilirim. Bu karsilastirmalari yapanlarin sonuclarindan boyle bir sonuc cikiyor. Ama bu karsilastirmalari yapanlar genelde programlarini tam denk yazamazlar. Aykiri durum kullanmadiklari program, aykiri durum kullanandan daha beceriksiz olabilir, vs. Yani cok az bir hiz kaybi olsa da, baska turlu kazanclar saglanmis olabilir.
Az once boyle bir program yazmaya kalkistim ama bilinmeyen cok sey oldugu icin vazgectim. Ornegin, tek bir hata oldugunda butun islemlerin gecersiz oldugunu mu dusunuyoruz, yoksa tek bir islemi mi yapmayacagiz?
Tek hata onemsizse:
for( /* ... */ )
{
try
{
islem();
}
catch (Hata const &)
{
// donguye devam et
}
}
Tek bir hata bile olayi bitiriyorsa:
try
{
for ( /* ... */ )
{
islem();
}
}
catch (Hata const &)
{
}
Bence bu iki program yapisinin hizlari farklidir. (Belki de degildir?)
Gercekten, bu sorunun kisa bir yaniti yok. Aykiri durumlarin yukarida siraladigim ve bazilarini unuttugum nedenler yuzunden program gelistirme zamanina olan etkisinin yararli oldugunu soyleyebiliriz. Program yavas kaliyorsa ve bunun nedeni aykiri durum kullanimiysa, o zaman programin belirli yerleri degistirilebilir.
Ancak, kullanmadan once verinin denetlenmesi de son derece normal bir yontemdir. Ornegin, kullanicinin tamsayi girmesi gereken bir durumda hic girisi denetlemeden matematiksel islem islevlerini cagirmamaliyiz.
Bazi durumlarda da aykiri durumlar cok dogaldir:
Sinif const & bastaki_ogeyi_ver()
{
if (topluluk_bos())
{
// basta oge yok, hicbir sey donduremem!
throw BirHata();
}
/* ... */
}
Ali
acehreli
19/11/2004, 02:11
ceeyt, belki de seni yanlis anladim ve baska fikirlere dallandim. Belki de tam olarak bolme islemiyle ilgili olarak soruyordun...
Sifirla bolme hatasini da kapsayan SIGFPE olustugunda ne olacagini signal isleviyle belirleyebiliriz. Ancak, signal'in belgesi (man signal) sifirla bolme durumunda ne olacaginin tanimsiz oldugunu soyluyor. Ortamina gore, boyle bir duruma dustugumuzde hicbir sey bile yapamayabiliriz.
Onun icin, bolenin sifir olmadigindan kendimiz emin olmaliyiz.
Ali
donanimsal olarak sormamistim,sormak istedigim buydu :) ;
Soru soyle olsun: Hatali durumlari bastan kendimiz denetlemek yerine, nasilsa ic islevlerden atilacak olan hatalara mi guvenmeliyiz? Boyle yaparsak bedeli ne olur?
gercekten cok guzel aciklamissiniz,tesekkur ederim...
Forum Yazılımı : vBulletin v3.6.8, Copyright ©2000-2008, Jelsoft Enterprises Ltd.