Polimorfizm ve C++

Merhaba sevgili yazılımperver dostlarım bugün sizler ile birlikte OOP’nin temel bileşenlerinden biri olan polimorfizme göz atacağız. Bu yazı ile birlikte aşağıdaki sorulara yanıt bulacağız. Kavramları olabilidiğince sade bir şekilde aktardıktan sonra bunları C++ da nasıl kullanıldığına bakıp, yine bu konu ile ilintili olduğunu düşündüğüm noktalara da değinip, konuyu kapatmayı planlıyorum açıkçası. Öncelikle sorularımıza bakalım:

  • Polimorfizm (“Polymorphism”) nedir ve nesne yönelimli yazılım geliştirme yaklaşımdaki yeri nedir?
  • Çeşitleri nelerdir? C++ da nasıl kullanılmaktadır?
  • C++’da sanal tabloları (“vTables”) kullanımı nedir?

Polimorfizm (“Polymorphism”) nedir ve nesne yönelimli yazılım geliştirmedeki yeri nedir?

Evet, ilk olarak polimorfizm nedir sorusunun cevabına bakalım isterseniz. Wikipedia’dan baktığımızda kısaca bir türün, başka bir tür gibi davranabilme ve bu tür gibi kullanabilme özelliği olarak ifade edilmiş. Kelimenin kendisine bakacak olursak “Polymorphism” kelimesi aslında “poly” ve “morphism” kelimelerinden bir araya gelmiştir. Bunlarda ilki çokluk ve ikincisi de form anlamına gelir. İlk olarak 1967 yıllarında Christopher Strachey denen kişi tarafından ortaya atılmış ve sonrasında ise Hindley ve Milner tarafından geliştirilmiştir.

Nesne yönelimli programlamada ise, bu özellik bir nesnenin, tek bir arayüze dayanmasına ve aynı metod ismine sahip olmasına rağmen farklı şekilde davranabilmesini ifade eder. C++ açısından bakacak olursak (ki buna ilerleyen başlıklarda daha detaylı bakacağız), bir sınıfın üye fonksiyonu çağırdığımızda, ilgili nesnenin tipine göre, farklı fonksiyonların çağrılabilmesi şeklinde gerçeklenmektedir. Peki bu nasıl olmakta? E bu kadar açıklama yeter hemen basit bir kod parçası ile duruma müdahale edelim 🙂 Bunu da hayvanlar alemi üzerinden yapalım.

Uygulamayı çalıştırdığımızda aşağıdaki çıktıyı alacağız:

Yukarıdaki örnekte göreceğiniz üzere aslında bütün sınıflar tek bir arayüz olan IAnimal arayüzünden türetilmekte. Fakat çalışma zamanında (ki burada main fonksiyonu içerisinde kallavi ilklendirme oluyor 🙂 ), oluşturulan nesne tipine göre daha sonra çalışma zamanındaki kullanım sırasında (ki buradaki aslında checkAnimals fonksiyonu) çağrılan fonksiyonların çıktısı ilgili nesneye göre farklılık gösteriyor. İyi de bunun bize ne faydası olacak diye düşündüğünüz anda hemen şu durumu hep birlikte ele alım. Diyelim ki, hayvanat bahçesinizi genişletmeye karar verdiniz ve yeni bir hayvan eklendi, neleri değiştirmeniz gerekecek? Aslında yukarıdaki basit örnek de bile, ilgili hayvan sınıfını ekleyip oluşturduktan sonra hiç bir şeyi değiştirmenize gerek kalmadığını göreceksiniz. İşte temel mantık bu. Elbette bu işin bir yönü, diğer yönlerine de hemen göz atacağız.

Çeşitleri nelerdir? C++ da nasıl kullanılmaktadır?

Şimdi artık biraz daha C++ doğru gelebiliriz. Bu yazımda Java ve benzeri dillere girmeyeceğim ağırlığı C++’a vereceğim ama kaynaklar kısmına bu dillere ilişkin de bir kaç sayfa koyacağım. Evet dostlar, C++ da polimorfizm kullanımına ilişkin bir örneği ilk başlıkta gördük. Bu aslında C++’ın çalışma zamanında bunu nasıl gerçekleştirdiği ile ilgiliydi buna benzer şekilde derleme zamanında da bu kabiliyet sunulmakta. Detaylarına geçmeden önce, C++’da bulunan polimorfizm mekanizmalarına göz atalım. Bunları aşağıdaki figürde görebilirsiniz:

Şimdi bu mekanizmalara daha yakından bakalım.

Çalışma Zamanında Polimorfizm

Çalışma zamanı polimorfizmi, kimi kaynaklarda dinamik polimorfizm olarak da geçer, temel olarak ilk örnekte de gösterildiği üzere fonksiyon üzerine bindirmesi (“function overriding“) ile gerçekleştirilmektedir. Bu da türetilmiş sınıfın, türetildiği sınıfta bulunan üye bir fonksiyona yeni bir tanım getirerek, bu fonksiyonun üzerine bindirmesi (“overriding“) ile gerçekleşir (evet türkçe karşılığına alışmak biraz vakit alabilir). Burada dinamik polimorfizmi gerçekleştirmek için “virtual” anahtar kelimesini kullanıyoruz, bu sayede derleyici, kullanılan işaretçinin tipinden ziyade, içerisinde gösterilen nesnenin tipine bakar ve bu sayede ilgili hayvanın metodu çağrılır. Velev ki virtual koymadık ne olur? Hemen yukarıdaki örneği virtual ve override anahtar kelimelerini kaldırarak çalıştıralım ve çıktıya bakalım:

Görmek istediğimiz elbette bu değildi. C++’da bu işin kotarılmasında sanal fonksiyonlar rol oynamakta. Derleyici için bu anhatar kelime, işaretçinin içeriğine bakması için gerekli uyarıyı veriyor. Bu noktada aslında sanal fonksiyonların nasıl çalıştığından da haberdar olmanın önemli olduğunu düşünüyorum, o sebeple bu konuyu ayrı bir başlıkta daha detaylı olarak ele alacağım.

Sonuç olarak çalışma zamanında gerçekleştirilen bu polimorfizm için şunları akılda tutmakta fayda var:

  • Bağlama (“Binding“) çalışma zamanında yapılır,
  • Sanal fonksiyonlar ve işaretçiler kullanılarak gerçekleştirilir,
  • Bu mekanizma ile dinamik (“dynamic“) ve geç (“late“) bağlama gerçeklenir,
  • Bu polimorfizm mekanizmasında, çağrı derleyici tarafından çözülmez,
  • Türetilen ve türetilmiş sınıflar arasındaki polimorfizm için metot ve parametreler aynı olmalıdır (fonksiyon imzaları),
  • Statik polimorfizme göre, görece yavaştır,
  • Çalışma zamanında çözümleme yapılmasından ötürü, daha fazla esneklik sunar.

Derleme Zamanında Polimorfizm

C++ da derleme zamanında polimorfizm iki şekilde yapılmakta bunlardan ilk fonksiyonlar, diğeri de operatörler kullanılarak gerçekleştirilmekte. Şimdi bunlara sırayla bakalım.

Fonksiyon çoklama temelde aynı isim ve dönüş değerine sahip fakat farklı sayıda, tipte parametre alma ve bunların farklı sıralanmaları ile gerçekleştirilir. Çok basit bir örnek vermek gerekirse:

Çalıştırdığınızda elde edeceğiniz çıktı:

Fonksiyon çoklamasına benzer şekilde C++’da belirli operatörleri de yukarıdakine benzer şekilde, temel ya da karmaşık tipler için çoklayabilirsiniz. Örneğin, “+” operaötürünü string sınıfları için, metinleri birleştirme amacı ile çoklayabilirsiniz ya da kompleks sayılar için toplamada kullanabilirsiniz. Buna ilişkin de hemen bir örneğe göz atalım:

Çalıştırdığınızda elde edeceğiniz çıktı:

Yukarıdaki örneklerden de anlaşılacağı üzere burada hangi fonksiyonların çağrılacağını aslında derleyici, derleme zamanında biliyor ve ona göre kodu üretiyor. Şimdi gelelim derleme zamanında gerçekleştirilen bu polimorfizm için aklımızda tutmamız gerekenlere:

  • Hangi metodun çağrılacağı derleme zamanında, derleyici tarafından belirlenir,
  • Fonksyion ve operatör çoklama kullanılarak gerçekleştirilir,
  • Bu mekanizma ile statik (“static“)  ve  erken(“early“) bağlama gerçeklenir,
  • Bu polimorfizm mekanizmasında, çağrı derleyici tarafından çözülür,
  • Aynı sınıf içerisinde, aynı isme sahip, fakat farklı tipte veya sayıda parametrelerin kullanılması ile gerçekleştirilir,
  • Dinamik polimorfizme göre, görece hızlıdır,
  • Derleme zamanında çözümleme yapılmasından ötürü, daha az esneklik sunar.

C++’da sanal tabloları (“vTables”) kullanımı nedir?

Şimdi geldik biraz daha teknik bir konuya. Aslında yukarıda anlatılan çalışma zamanındaki polimorfizmin C++ da gerçeklenmesindeki en büyük rol sanal fonksiyonlarda. Eminim meraklı yazılımperver dostlarım, hayatlarında bir noktada aşağıdaki kodları çalıştırdıklarında ortaya çıkan farkı merak edip bu yapılara ulaşmışlardır. Aslına bakarsanız, benim de mülakatlarda arada sorduğum sorulardandır 🙂

Çalıştırdığınızda elde edeceğiniz çıktı:

Sanal fonksiyonların da çalışmasının arkasında yatan mekanizma ise vtables olarak da bilinen sanal tablolardadır “virtual tables“. Haydi şimdi bunlara yakından bir göz atalım ne dersiniz. Peki nedir bu vTable‘lar? Wikipedia ne der bakalım:

A virtual method table (VMT),…, is a mechanism used in a programming language to support dynamic dispatch.

Kısaca, çalışma zamanında polmorfizm kapsamında, ilgili nesneye ilişkin doğru fonksiyonun seçilmesinde kullanılan tablodur. Peki normalde bu iş nasıl yapılmakta, hemen bakalım (aslında yukarıda biraz buna değinmiş olduk ama biraz daha basit bir örnek üzerinden gidelim):

Derleyici yukarıda tanımlamış olduğumuz Base sınıfımızda bulunan func1()’ı oluşturacak ve bunun adresini hatırlayacak ve her Base sınıf nesnesinde aynı kod parçası çağılıyor olacak, yani belirsiz bir durum yok ve çalışma zamanı için herhangi bir sıkıntı. Şimdi ortamı biraz daha renklendirelim 🙂 mesela ortama bir kaç sanal fonksiyon ve dahi türetilmiş bir sınıf ekleyelim:

Bu durumda eğer Base sınıftaki virtual anahtar kelimeleri silersek aslında yine bir önceki örnekte olduğu gibi Base sınıfının fonksiyonu çağrılıyor olacaktı ama burada durum farklı. Burada, çalışma zamanında uygun fonksiyon seçilerek çağrılıyor olması gerekiyor. Şimdi adım adım bunların nasıl gerçekleştirildiğine bakalım:

  • Ne zaman ki sınıfınızda bir sanal fonksiyon tanımladığınızda, derleyici sınıf içerisinde bir sanal tablo (bundan sonra vTable diyeceğimoluşturur. Aynı zamanda türetilen sınıflar içerisinde de ayrıca bir vTable bulunmakta. Yukarıdaki boyuttaki fark da bundan kaynaklanıyor bu arada,
  • Sınıfa ait bütün nesneler aynı vTable‘ı paylaşır,
  • vtable içerisinde, sınıf içerisindeki her bir sanal fonksiyon için bir kalem tutulur ve burada ilgili fonksiyonun en spesifik hali tutulur (örnek Derived::func1()). Yukarıdaki örnek için aşağıdaki gibi vTable‘lar oluşturulur:
  • Şimdi bu tabloları inceleyecek olursak.
    • Base sınıfına ait vTable içerisinde iki adet girdi var bunlar bu sınıf içerisinde tanımlanmış olan func1() ve func2() fonksiyonları. Burada, iki fonksiyon da kendilerini işaret etmekte çünkü, Base sınıf gözünden, bu fonksiyonların en spesifik halleri bunlar
    • Gelelim Derived sınıfına ait vTable’a. Burada func1(), Derived sınıfına ait ilgili fonksiyonu işaret etmekte. Çünkü bu sınıf Base sınıftaki ilgili fonksiyondan daha spesifik. Fakat, func2()’ye baktığımızda, bu fonksiyona ilişkin girdi, Derived değil de, Base sınıfına ait func2() fonksiyonunu işaret etmekte, çünkü Derived içerisindeki func2() ilgili fonksiyonun üzerine bindirme yapmıyor. Bu durumda en spesifik hali Base sınıfınki oluyor,
  • Derleyici, bu tablolar için her bir sınıfa, bu tabloya olan bir işaretçi ekler ve tahmin edebileceğiniz üzere çalışma zamanında da bu tabloya göre doğru fonksiyon seçilerek çağrılır. Yani nihayetinde sınıflar aşağıdaki hali alırlar:

Polimorfizm açısından, vTable’a ilişkin yukarıdaki açıklamalar umarım yeterli olmuştur. Elbette C++ için bütün hikaye bu değil ama bundan sonraki mecra biraz daha teknik detaylara giriyor ki açıkçası burada oraya dalma niyetim yok ama kısaca bahsedeyim. Bir sonraki adım aslında “Multiple Inheritance”, yani çoklu miras. Bu durumda ise birden fazla sınıftan türetilen sınıf için birden fazla vTable ihtiyacı olabilir. Hatta miras da ortak atadan gelen sınıflardan türetilme durumlarında, daha da fazla vTable ihitiyacı olabilmektedir.

vTable’lara ilişkin daha detaylı bilgi ve bir kaç kod parçası için kaynaklar kısmına bir kaç kaynak ekliyorum. Meraklı yazılımperver dostlar bir göz atabilirsiniz.

Evet dostlar bir yazımın daha sonuna geldik. Umarım yazı, C++’da polimorfizm ve genel olarak polimorfizm konularına açıklık getirmiştir.

Bir sonraki yazımda görüşmek dileğiyle. Bol kodlu ve sağlıklı günler diliyorum 🙂

Kaynaklar

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Bu site, istenmeyenleri azaltmak için Akismet kullanıyor. Yorum verilerinizin nasıl işlendiği hakkında daha fazla bilgi edinin.