Çalışma Zamanı Çokbiçimliliği ve Sanal Fonksiyonlar (Runtime polymorphism and virtual functions) - 1

C++ ve aslında nesneye dayalı programlamada karşımıza çıkan çalışma zamanı çokbiçimliliğini sağlayabilmek için kullanacağımız virtual anahtar kelimesi hangi nedenlerle kullanılmaktadır? Kullanım biçimleri nelerdir? sorularını cevaplamadan önce basitçe şunu söylemek gerekirse:

Polymorphism türleri
Polymorphism temel olarak ikiye ayrılmaktadır.
  1. Derleme zamanı çokbiçimliliği (Fonksiyon ve operatör aşırı yüklenmesi ile)
  2. Çalışma zamanı çokbiçimliliği (Sanal ve saf sanal fonksiyon ile)

Aşağıda anlatacağımız yöntemlerin tamamı çalışma zamanı çokbiçimliliğine ilişkindir. Aşağıdaki örnekte taban ve türeyen sınıf içinde foo isimli fonksiyonlar bulunmakta ve türeyen sınıfın taban sınıfın referansı ile adreslenebildiği görülebilmektedir. Buna literatürde Liskov’un Yerine Geçme Prensibi (Liskov Substitution Principle) denilmekte ve bu prensibe göre Alt sınıflardan oluşturulan nesneler üst sınıfların nesneleriyle yer değiştirdiklerinde / işaretçileriyle adreslendiğinde nesnenin kendi davranışını göstermek zorunda olduklarını belirtir. Peki aşağıdaki örnek bu prensibe uygun mudur?


Çalıştırılıp çıktıya bakılırsa iki durumda da taban sınıfın foo() fonksiyonu çağrılmaktadır. Bu durumda çok biçimlilik ilkesi gerçekleşmemiş her durumda taban sınıfın fonksiyonu çağrılmıştır. Bu durum nasıl düzeltebilir? derseniz karşımıza virtual anahtar kelimesi çıkar. Taban sınıfın ilgili fonksiyonu türeyen sınıfta override edileceğini belirtmek için virtual anahtar kelimesi kullanılmış türeyen sınıfta ise C++11 ile gelen override anahtar kelimesi ile (Zorunlu değil yazmazsak benzer davranışı gösterir ama yazılması tavsiye edilmektedir.) fonksiyon override edilmiştir.


Şimdi programı çalıştırıp çıktıya bakılırsa bu durumda istenen çıktının sağlandığı ve 24. kod satırı için Base::Foo, 25. satır için Derived::Foo çıktısı alınacaktır. Yukarıdaki örnekte türeyen sınıfın overide edilip edilmeyeceğine ilişkin bir zorunluluk bulunmamaktadır. Ama türeyen sınıfa böyle bir zorunluluk getirmek istenirse saf sanal (pure virtual) 'lığın kullanılması gerekmektedir. Bunun için yapılması gereken fonksiyon bildirimi ve =0; biçiminde aşağıdaki gibi kullanılması gerekmektedir.


Bu durumda türeyen sınıfta override edilmezse aşağıdaki gibi derleme hatası ile karşılaşılmaktadır. Bu noktada C++’ta saf sanal fonksiyonların bulunduğu sınıflara Java/C# 'taki gibi abstract sınıflar diyebiliriz ve benzer şekilde abstract sınıflar üzerinden nesne yaratılamamaktadır.


Aşağıda abstract class ve türeyen sınıfı içeren kod yazıldığında sorunsuz çalıştığı ve beklenen çıktının alındığı görülmektedir.


Son olarak çok biçimliliğe ve çok biçimliliğin faydalarına ilişkin bir örnek vermek gerekirse aşağıdaki örnekte olduğu gibi Caller fonksiyonuna yapılan çağrının, farklı nesnelerde nesnenin tipine bağlı olarak farklı fonksiyonların çağrılmasını sağlamıştır. Ve bu sayede çok biçimlilik sağlanmıştır. Ve uzun vadede türeyen sınıfın üstüne bir tane daha türetme yapılsa bile Caller fonksiyonunda bir değişiklik yapmaya gerek olmamaktadır ve geleceğe uyumlu kod yazabilme olanağı sağlamıştır.


Bitirirken şu noktaya dikkat çekmek gerekirse çalışma zamanı çok biçimliliği doğru zamanlarda kullanılmalıdır ve performans maliyeti olduğu akıldan çıkarılmamalıdır. Maliyetin sebebi; çalışma zamanı çok biçimliliği sağlayan late binding ya da diğer adıyla dynamic binding işleminin çalışma zamanında virtual table (C++ standartları polymorphism'in nasıl gerçekleşeceğini tanımlamaz. Ancak tüm derleyiciler virtual table yöntemini kullanmaktadır.) mekanizması yoluyla gerçekleştirilmesi ve bunun da maliyetli bir işlem olduğu akıllardan çıkartılmamalıdır.


Algorithm | find_first_of, find_end, adjacent_find

C++'da bir veri yapısı içinde arama yapmak için kullanılabilecek std::find_first_of, std::find_end, std::adjacent_find algoritmalarını sırasıyla anlatmaya çalışalım.

Algorithm | find_first_of

find_first_of algoritmasının template bildirimi
std::find_first_of algoritması C++11 ile gelen bir algoritma ve çalışma prensibi bir veri yapısı içinde başka bir veri yapısının elemanlarını arama ve bulduğunda iteratör değeri ile geri dönme üzerinedir. find_first_of çağrısı sırasında verilen ilk iki parametre aranılan veri kümesine ilişkin aralık belirten iteratörler, sonraki iki parametre ise aranacak veri yapısına ilişkin başlangıç - bitiş iteratör değerleridir. Ve 5. parametre olarak Binary Predicate bir callable ifade de karşılaştırma için yazılabilmektedir.

Algorithm | find_first_of örneği - 1
Yukarıdaki örnek find_first_of algoritmasının temel kullanımına ilişkindir. v vektör container'ı içinde t container 'ında olan değerlerden herhangi biri bulunmaya çalışılır. Yukarıdaki örnekte hiçbir değer bulunamayacaktır. Aşağıdaki örnek ise string sınıfı içindeki find_first_of algoritmasının kullanımına ilişkindir. string sınıfı içindeki find_first_of çağrısı sırasında verilen string parametre içindeki değerlerden herhangi birinin bulunmasına ilişkin arama yapılmasını ve bulduğunda ilgili değere ilişkin iteratör değeri ile geri dönülmesini sağlamaktadır.

Algorithm | find_first_of örneği - 2
Yukarıdaki örnekte str string'i içerisinde O, U, T değerlerinden herhangi birinin str içinde geçtiği yerleri bulan ve bunları küçük harflerle değiştirmektedir.

Algorithm | find_first_of örneği - 3
Yukarıdaki örnekte 5. parametre olarak Binary Predicate bir callable olarak lambda ifade tanımlanmış ve 2, 3, 4 'den herhangi birine tam olarak bölünebilen ilk sayı bulunmuştur.

Algorithm | find_end

find_end algoritmasının template bildirimi
std::find_end algoritmasının çalışma prensibi bir veri yapısı içinde başka bir veri yapısının elemanlarını bütünsel ve ardışıl olarak arama ve bulduğunda ilk elemanın iteratör değeri ile geri dönmesi üzerinedir. find_end çağrısı sırasında verilen ilk iki parametre aranılan veri kümesine ilişkin aralık belirten iteratörler, sonraki iki parametre ise aranacak veri yapısına ilişkin başlangıç - bitiş iteratör değerleridir.

Algorithm | find_end örneği - 1
Yukarıdaki örnekte 2, 3, 4 'ün ardışıl olarak sıralandığı son yer alan 8 indeks değerini ekrana basacaktır. Aşağıdaki örnek ise . ifadesinin string içinde son geçtiği yerin bulunmasını sağlamaktadır.

Algorithm | find_end örneği - 2

Algorithm | adjacent_find

adjacent_find algoritmasının template bildirimi
adjacent 'in kelime anlamı bitişik'tir. Ve std::adjacent_find algoritmasının çalışma prensibi de tam olarak buna karşılık gelmektedir. Yani bir veri yapısı içinde elemanlarını ardışıl olarak tekrar edip etmediğine bakmakta ve tekrar eden ilk değeri bulduğunda elemanın iteratör değeri ile geri dönmektedir. adjacent_find çağrısı sırasında verilen ilk iki parametre kontrolü yapılan veri yapısına ilişkin aralık belirten iteratör değerleridir. 3. parametre ise yazılmazsa varsayılan karşılaştırma; binary predicate callable bir ifade yazılmışsa ona havale edilmektedir.

Algorithm | adjacent_find örneği - 1
Yukarıdaki örnekte string içerisinde tekrar eden harfler listelenmektedir. Aşağıdaki örnekte ise benzer işlem 3. parametrede Binary Predicate bir lambda ifade üzerinden sağlanmaktadır.

Algorithm | adjacent_find örneği - 2



std::string | Karakter silme işlemleri

C++ ile string ifadeler içinde silme/ekleme yapabilmek için kullanabileceğimiz erase fonksiyonunun temel olarak 3 farklı çağırma biçimi vardır. Şimdi çağırma biçimlerini sırasıyla inceleyelim.

erase fonksiyonu template bildirimi

Karakter dizisi içinde bir bölümü silme

Aşağıdaki örnek yukarıda üç gruba ayrılmış olan erase'in ilk template bildirimine ilişkin olup erase çağrısı sırasında verilen iki parametreden birincisi string veri yapısı içerisinde hangi indeks değerinden itibaren sileceğini, ikinci parametre ise silinecek parçanın uzunluğunu ifade eder.  2. parametre varsayılan arguman olarak std::string::npos olarak bildirilmiştir.  Bu sayede 2. parametreyi geçirmezsek 1. parametrede verilen değerden başlayarak ifadenin sonuna kadar silme işlemini gerçekleştirir. 1. parametreye varsayılan arguman geçildiğinde ise silme işlemi  0 indeks değerinden başlamaktadır.  Son olarak bu grupta yer alan erase 'in geri dönüş değeri ise *this nesnesidir. 

erase örneği - 1
Yukarıdaki kullanım en temel kullanım biçimidir ve sadece 1. parametre geçilmiştir. Bu da yukarıda anlattığımız gibi ifadenin sonuna kadar silecektir. Aşağıda ise programın çıktısı görülmekte ve beklenen şekilde 16.  indeks değerinden itibaren silme işlemi gerçekleşmiştir. 

örnek - 1 : çıktı
Aşağıdaki örnek C++11 ile gelen 3. template bildirimi örneğine ilişkin olup erase çağrısı sırasında verilen iki parametreden birincisi başlangıç iteratör değeri, 2. parametre ise  silinecek son elemanın iteratör değeri şeklindedir. Bir aralık silmek için iteratör değeri kullanmak gerekmektedir.

erase örneği - 2
Yukarıdaki örnekte başlangıçtan 4 ilerideki iteratör değerinden başlayarak, son değerden bir sonrakini gösteren end iteratör değerinden 9 gerisine kadar bölgenin silinmesi istenmektedir. Aşağıda ise beklenen çıktı görülebilmektedir.

örnek - 2 : çıktı

Karakter dizisi içinde bir noktayı silme

Aşağıdaki örnek ise C++11 ile gelen 2. grup template bildirimi örneğine ilişkin erase çağrı türü olup parametre olarak iteratör değeri geçirilerek sadece 1 değerin silinmesi sağlanmıştır.

erase örneği - 3
Aşağıda ise programın çıktısı görülmekte ve beklenen şekilde 1.  indeks değerinin silinme işlemi gerçekleştiği görülmektedir.

örnek - 3  : çıktı

erase ile birlikte çeşitli kullanım idiyomları

Aşağıdaki örnek find - erase ikilisi ile belirli bir karakterin tümünün string ifade içinden silinmesine ilişkindir. find fonksiyonu ile silinmek istenen karakterin indeks değerinin bulunması ve indeks değeri ile erase çağrısının gerçekleştirilmesi üzerinedir. Bu sırada erase çağrısına 2. parametre olarak 1 değerinin geçilmesi sadece bir karakterin silinmek istenmesinden dolayıdır. Aksi halde string'i sonuna kadar sileceğini yukarıda açıklamıştık.

erase örneği - 4
Aşağıda ise programın çalışmasını ve çıktısının beklenen şekilde olduğu rahatlıkla görülmektedir.

örnek - 4 : çıktı
Aşağıdaki örnekte ise yukarıdaki gibi döngü kullanmadan remove - erase idiyomu ile yukarıdaki gibi istenen belirli karakterin tamamı silinebilmektedir.

erase örneği - 5
Aşağıda ise örnek - 4 'e benzer bir çıktı sağlandığı görülebilmektedir. 

örnek - 5 : çıktı
Aşağıdaki örnekte ise remove_if - erase beraber kullanılarak string içindeki rakamların tamamı silinmiştir.

erase örneği - 6
Programın çıktısı ise aşağıdaki gibidir.

örnek - 6 : çıktı
Aşağıdaki örnekte ise insert - erase beraber kullanılarak string içindeki belli bir blok silinmiş ve yerine karakter ekleme yapılmıştır.

erase örneği - 7
Aşağıda program çıktısı olarak 2017 'nin silindiği yerine X ifadesinin eklendiği görülmektedir.

örnek - 7 : çıktı

Algorithm | find, find_if, find_if_not

C++'da bir veri yapısı içinde arama yapmak için kullanılabilecek std::find, std::find_if, std::find_if_not  algoritmalarını sırasıyla anlatmaya çalışalım.

Algoritmaların template bildirimleri

Algorithm | find

std::find çağrısı sırasında verilen ilk iki parametre aralık belirten iteratorler ve 3. parametre C++17 'ye kadar sabit bir değer biçimindedir.

Algorithm | find örneği
find algoritması 3. parametrede verilen değeri baz alarak başlangıç iteretöründe gösterilen değerle başlayarak karşılaştırma yapar ve eşitlik koşulu sağlandığında değerin iteratör karşılığı ile geri döner. Bulamazsa veri yapısının end() fonksiyonu karşılığı olan iteratör değeri ile geri döner. Yukarıdaki örnekte bir vectör dizisi içinde console'dan girilen değerin var olup olmadığını bulan ve kaç tane varsa ekrana yazan programdır.

Algorithm | find_if

std::find_if çağrısı sırasında verilen ilk iki parametre aralık belirten iteratorler ve 3. parametre C++17 'ye kadar Unary Predicate bir callable değer biçimindedir. find_if algoritması 3. parametrede verilen callable ifadenin başlangıç iteretöründe gösterilen değerle başlayarak işleme sokar ve callable true döndüğünde bulduğu değerin iteratörü ile geri döner. Bulamazsa veri yapısının end() fonksiyonu karşılığı olan iteratör değeri ile geri döner. Aşağıdaki örnekte vector içindeki çift sayılar bulunup ekrana basılmaktadır.

Algorithm | find_if örneği - 1
Yukarıdaki ve aşağıdaki örnekte callable olarak lambda 'lar kullanılmıştır. Aşağıdaki örnekte lambda bir değişken olarak tanımlanmış ve find_if 'e öyle geçirilmiştir.

Algorithm | find_if örneği - 2
Aşağıdaki örnekte ise callable olarak fonksiyon kullanılmıştır. Yukarıdaki ve aşağıdaki örnekte string ifade içerisindeki büyük harfleri bulup ekrana basmaktadır.

Algorithm | find_if örneği - 3

Algorithm | find_if_not

std::find_if_not C++11 ile gelen ve kabaca find_if'in tersi biçimde çalışan bir algoritmadır. find_if _not algoritması 3. parametrede verilen karşılaştırma işlemi yapan callable ile başlangıç iteretöründe gösterilen değerle başlayarak işlem yapar ve callable false döndüğünde değerin iterotörü ile geri döner. Şartı sağlayan bir ifade bulamazsa veri yapısının end() fonksiyonu karşılığı olan iteratör değeri ile geri döner.

Algorithm | find_not örneği

Yukarıdaki örnekte vektör dizisi içerisindeki tek sayılar ekrana basılmaktadır.