Akıllı Göstergeler (smart pointers)

Cansiz

New member
Giriş

Bu yazıyı daha önce bazı forumlara gönderdiğimi düşündüğüm bir mektuptan uyarladım. Arattığımda ne ceviz.net'te ne de cdili'nde bulamadığıma bakılırsa belki artık yaşamayan
ocal.net'e göndermişimdir.

Yazıda akıllı göstergeleri tanıtıyor ve bir akıllı gösterge uyarlaması veriyorum.

Daha sonra C++'ın standardında bulunan auto_ptr'ı tanıtıyor ve onun yetersiz kaldığı durumlar için başka kütüphaneler öneriyorum.


Nedenler

Göstergelerin (pointer) öğrenilmeleri kadar, hatasız kullanılmaları da zordur. Hatta bence C programcılarının hemen hemen her gün yaşadıkları bir güçlük doğrudan
göstergelerle ilgilidir:



int foo0() {
char * p0, *p1;
p0 = malloc(1);
if (birSeylerYap(p0))
{ free(p0);
return 1;
}
p1 = malloc(1);
baskaSeylerYap(p0, p1);
free(p1); free(p0);
return 0;}

Yukarıdaki 'foo0' işlevinde bellek sızıntısı olmaması, programcının dikkatine kalmıştır. İşlevden çıkılan her noktada bellekten ayrılan bütün alanların geri verilmelerinin unutulmaması gerekir.

Bu kadar küçük bir işlevde sorunun büyüklüğü pek anlaşılmıyor olabilir. 'foo0'ın p2 adında bir göstergenin gösterdiği yeni bir bellek ayırması gerektiğini; hatta
işlevden çıkılabilecek yeni bir 'return' deyimi eklendiğini düşünün. Kod gittikçe daha karmaşık ve hatasız yazılması çok daha güç bir hale gelir. Her C programcısı bu tür işlevlerle karşılaşmıştır...

Daha ileri gitmeden önce, haksızlık olmasın diye, 'foo0'ı daha deneyimli bir programcının yazacağı şekliyle vereyim:



int foo0_dahaIyi()
{
char * p0; p0 = malloc(1);
if (birSeylerYap(p0) == 0)
{
char * p1 = malloc(1);
baskaSeylerYap(p0, p1);
free(p1); return 1;
}
free(p0);
return 0;
}

Bu işlev bir öncekinden daha iyidir; çünkü

1) p1'in tanımı, kullanılmasının gerektiği ilk noktaya en yakın yere taşınmış durumdadır. Kodu okuyan kişilerin 'if' deyiminin içine bakmadan önce p1'den haberlerinin olması gerekmiyor.

2) Kaynak ayıran her bir deyime (malloc) karşılık kaynağı geri veren yalnızca bir tane deyim (free) görünüyor.

3) İşlev bu haliyle yeniden yapılandırmaya (refactorization) çok daha yatkındır. if deyiminin içindeki satırları oldukları gibi alıp başka bir işlevin içine kolayca taşıyabiliriz. Böylece kodun yapısı çok daha açık bir hale gelir.

Biraz konudan ayrılmış olsam da, buraya kadar söylemek istediklerim, C'de kaynak denetiminin programcıya bırakılmış olduğu ve doğru kullanımının programcının dikkatini
gerektirdiğidir.

Ayrıca söylediklerimin bellek denetimi ile kısıtlı olmadığına da dikkatinizi çekmek istiyorum. malloc satırlarını fopen'la, free satırlarını da fclose'la değiştirirseniz; sorun, bellek sızıntısı yerine kütük belirteci sızıntısı halini alır.

Bence bir C programcısı için C++'ın en çekici yanı, sınıfların kurucu ve bozucu işlevlerini kullanarak bütün bu kaynak sızıntılarından kurtulunuyor olmasıdır.

Biz insanlar her durumda hata yapmayı becerdiğimiz için, C++'ta da kaynak sızıntıları yaşarız. Ancak bunların çoğu, C++'ı C'den kalan alışkanlıklarımızla kullandığımız içindir. Hatta, eğer C++'ı C gibi kullanırsak, en azından kaynak sızıntıları açısından daha da kötü bir durumdayızdır. Bunun nedeni, C++'ın aykırı durum düzeneğidir (exception handling mechanism).

Aşağıdaki işlev, bir C programı içinde doğru yazılmış bir işlev olarak anılabilecekken, bir C++ programı içinde kullanıldığında hatalıdır:

void foo1()
{
char * p = malloc(1);
bar(p);
free(p);
}
Çünkü C++'ta bir işlevden çıkmanın açıkça görülmeyen yolları da vardır. Örneğin 'bar' işlevinin çağrılması sırasında atılacak bir aykırı durum, bu işlevden free'nin çağrıldığı
satır işletilmeden çıkılmasına neden olur. İşte C'de olmayan ama C++'ta her an karşılaşabileceğimiz bir kaynak sızıntısı budur.

Not 1: Söylediğimin aykırı durum atmayacağı bilinen 'bar' işlevleri için geçerli olmadığını biliyorum. Standart kütüphanede böyle işlevler var. Ben 'bar'la, genel bir işlevi kastediyorum. Bir projenin gelişmesi sırasında, 'bar'ın veya onun çağırdığı başka bir işlevin eninde sonunda aykırı durum atıyor olması olasılığı olduğu için, foo1 riskli bir işlevdir.

Vurgulamak istediğim, her işlevi aykırı durum atması olası bir işlev olarak görüp, hiçbir C++ programında foo1'deki gibi bir işlev yazmamamızın gerektiğidir.

Not 2: Bu örnekte malloc yerine new, free yerine de delete kullanmak hiçbir şeyi değiştirmez. 'bar'ın aykırı durum atması ve bizim delete satırına hiçbir zaman erişemeyebilecek olmamız o durumda da geçerlidir.


Tekrar söylüyorum: Burada söylediklerim, yalnızca bellek denetimiyle ilgili değil: başka kaynakları ayıran ve geri veren satırları da hiçbir zaman kodumuz içinde birbirlerinden böyle ayrı olarak yazamayız.

Böyle durumlarda yapmamız gereken, Stroustrup'un "resource allocation is initialization (RAII)" olarak adlandırdığı, ama daha iyi anlaşılır olması için keşke "resource
deallocation is destruction" deseymiş diye düşündüren temel bir C++ yöntemini uygulamaktır.

Yukarıdaki İngilizce terimleri az sonra anlatacaklarıma uysunlar diye 'kurma' ve 'bozma' kavramlarını da içerecek şekilde sırasıyla şöyle çevirebilirim: "kaynak ayırmak,
aslında kurmaktır" ve "kaynağı geri vermek, aslında bozmaktır."

Daha da açık olmak için bu yöntemi şöyle özetleyebiliriz:
"kaynaklar nesnelerin bozucu işlevlerinde geri verilmelidirler."

Bunun nedeni, C++'ta bir kapsamdan (örneğin '{' ve '}' parantezlerinin arasındaki bir kod bloğundan) nasıl çıkılırsa çıkılsın, o kapsamda tanımlanmış olan otomatik
değişkenlerin bozucu işlevlerinin mutlaka çağrılacak oluşudur. Yani, bir işlevden bir aykırı durum nedeniyle de çıkılıyor olsa, nesnelerin bozucu işlevleri mutlaka
çağrılırlar.

Standart kütüphanede RAII yönteminden yararlanan sayısız sınıf ve sınıf şablonu vardır. Örneğin string, karakterleri tuttuğu bellek alanını bozucu işlevinde geriye verir;
ofstream, açtığı kütüğü bozucu işlevinde kapatır; vs. Bu tür 'akıllı sınıflar', kendi başlarının çaresine bakmayı bilirler.

'Akıllı gösterge' deyimi de bir nesneyi göstermenin yanında, kendi temizliğini yapma gibi ek işleri de beceren sınıflar için kullanılır.

Ne yazık ki standart C++ kütüphanesi akıllı göstergelerden yana çok şanssızdır. Standardın içindeki tek akıllı gösterge olan auto_ptr'ın kullanılabilirliği çok kısıtlıdır. Hatta, aşağıda değineceğim kullanım amacının dışında kullanılması da yarardan çok zarar getirir.

Yazının geri kalanına kendimiz bir akıllı gösterge tasarlayarak devam edelim. Bu tasarımı yaparken, bir kaç noktada her duruma uygun bir karar vermenin olanaksız
olduğunu görecek ve seçeneklerden birisinde karar kılmak zorunda kalacağız. Böylece auto_ptr'ın neden her işe yatkın olmadığını, ve C++ standartlaştırma komitesinin almak
zorunda olduğu kararlar sonucunda son halini aldığını anlayacaksınız.

Amacımız, bir gösterge gibi kullanılabilecek bir sınıf tasarlamak... Bu sınıf, program içinde tıpkı bir gösterge gibi kullanılabilsin; buna ek olarak, sorumlusu olduğu
nesneyi de zamanı geldiğinde doğru bir şekilde temizlesin... Ayrıca, işe bu akıllı göstergeyi 'char' türünden bir nesneyi gösterecek şekilde tasarlayarak
başlayalım. Daha sonra bu sınıfı kolaylıkla bir sınıf şablonuna dönüştürecek ve böylece daha kullanışlı bir hale getireceğiz.

Akıllı gösterge sınıfımızın adı 'Gosterge' olsun...
Not: Aşağıda 'yalın gösterge' ile, C ve C++'ın herhangi bir sınıf içine alınmadan açıkça kullanılan sıradan göstergelerini kastedeceğim. Örneğin,

char * p;

gibi bir satırda tanımlanan p, yalın bir göstergedir.

Akıllı göstergemizi tasarlarken, yalın bir göstergenin ne gibi durumlarda kullanıldığına ve onunla ne yapabildiğimize bakalım:

Alıntıdır!
 

HTML

Üst