Ç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.