C++ Copy Constructor

Soru cevap olarak gidersek konuyu daha iyi anlayabileceğimizi ve anlatabileceğimi umuyorum.

Türkçe olarak nasıl çevirebiliriz?
~> Yoğun olarak Kopyalan Kurucu İşlev olarak çevirildiğini gördüm. Genelde forumlar ve bloglarda kısaltmasını görmeniz mümkün. Kısaltması CC.

Copy Constructor Hikayesi Nedir?
~> Öyle durumlar var ki, bir nesne hayata değerini başka bir nesneden alarak başlıyor. Böylesi durumlarda hayata gelen nesne için çağrılan constructor a COPY CONSTRUCTOR deniliyor.

Aşağıdaki örneği inceleyelim:
Bu örnekte C++03 ve C++11 kullanımlarına da örnek verelim.

Copy Constructor

Gördüğünüz gibi m1 oluşturulduktan sonra m2 nesnesini m1 ile oluşturuyorum. Burada copy constructor devreye giriyor.

İkinci örneğe baktıktan sonra biraz daha net anlayacağız.

Copy Constructor

Burada anlayabilmek için this göstericisi ve adresleri ekrana yazdırdım.

Peki Copy Constructor Yapısı Nasıl?
~> Copy Constructor kullanımını örnekte görmüş olduk. Biraz açıklayalım. Sınıf ismi ile aynı fakat diğer nesneye de ulaşmamız gerekiyor. Nesneye ulaşmam için onu referans yoluyla almam gerekiyor ve nesneyi okuma amacıyla kullanacağım için const olması gerekiyor. Fonksiyonumuzun yapısıda böyle.

Ne zaman Copy Constructor çağırılıyor?
~> Şimdi az önce söylediğimiz gibi bir nesne hayata kendi türünden başka bir nesnenin değerini alarak geldiğinde çağırılıyordu. İşte burada bir soru daha aklımıza geliyor.

Hangi durumlarda Copy Constructor çağırılıyor?
~> 3 tipik senaryo var.

Senaryo 1 ~> Açık ilk değer verme, (ilk değer verme çeşitlerini 1. örnekte yazdım)

Senaryo 2 -> Bir fonksiyon var, bu sınıfın parametresi bir sınıf türünden. (Dikkat sınıf türünden diyorum, sınıf türünden bir pointer veya referans değil) Bahsettiğim call by value, call by reference değil.

Senaryo 2 için bir örnek verelim.

Copy Constructor
/*
default constructor
&m1 = 0x7fff5fbff768
***********************************
copy constructor
this = 0x7fff5fbff760
&r = 0x7fff5fbff768
***********************************
gfunc çağrıldı
&m = 0x7fff5fbff760
— — — — — — — — — — — — — — — — — — — — — — -
*/

Senaryo 3 -> Biraz daha zor bir senaryo. Çünkü ortada bir nesne görünmüyor :) Ama makina koduna yada assembl koduna bakılacak olursa orada görülecektir. Görünmemesinin sebebi bu isimlendirilmiş bir nesne değil. Bu durumda hayata gelen nesne fonksiyonun geri dönüş değerini tutacak olan geçici nesnedir. O zaman *this burada geçici nesne.

Fonksiyonlar bir türe geri döndüğünde (int * yada int & demek istemiyorum) çağırılan fonksiyonun, çağıran fonksiyona değer iletmesi için bizim görmediğimiz bir nesne hayata geliyor.

Peki bu nesne değerini nereden alıyor?
~> return expression dan alıyor.

Bu geçici nesneye değer atama olarak mı gidiyor yoksa ilk değer olarak mı veriliyor?
~> ilk değer olarak veriliyor.

Senaryo 3 için bir örnek verelim.

/*
default constructor
main basliyor
***********************************
copy constructor
***********************************
destructor
main bitiyor
destruct
*/

~> Burada default const. global nesne için çağırıldı. Global olduğu için main den önce çağırıldı.

Peki neden copy constructor çağırıldı, nereden çıktı? Çünkü ortada herhangi bir nesne yok. Anlatmak istediğim nokta burası. Bu örnekte bir destructor yazdık. Anlatmak istediğim konu biraz daha anlaşılıyor hale geliyor. Yani bu örnekte 2 kez destructor çağırılıyor. 2. destructor sizin görmediğiniz nesnenin..

Copy Constructor biz yazmazsak derleyici bizim için bir CC yazacak mı?
~>Evet compiler bizim için bir CC yazacak.

Derleyicimizin yazdığı CC non-static inline public üye fonksiyonudur. Tıpkı constructor ve destructor da olduğu gibi.

CC bizim tarafımızdan yazılması gereken durumlar var. Durumu basite indirgeyerek şöyle anlatabilirim. Eğer derleyicinin yazdığı destructor ile yetinemiyorsak yani kaynakların geri iadesi için destructor yazıyorsak kesinlikle copy constructor ı biz yazmalıyız. C++11 öncesinde destructor siz yazarsanız CC yazmamanız bir sentaks hatası değil. C++11 de bunu depracated ilan ettiler.

Yani kısaca şöyle açıklayabiliriz, destructor var ise demek ki kaynaklar geri verme işi var. O zaman kaynakları release etme gibi bir tema varsa sınıf nesneleri birbirine kopyalandığında biz pointerları yada referansları kopyalamış oluyoruz. Buda bağımsızlık ilkesini tamamen bozar. Bağımsızlığı kendin elde edeceksin. Nasıl olucak bu iş? CC kendin yazarak.

Konuyu açıklayan birkaç örnek yazmaya çalışalım. Şöyle bir senaryo oluşturuyorum.
Bir tane int türden öğe olsun, ismin uzunluğunu bulsun(m_len).İsmi dinamik bellek alanında tutalım. Constructor m_len e p pointerinin gösterdiği ismin uzunluğunu hesaplasın, m_p pointerinin m_len+1 karakterlik bir heap te bellek alanını edinsin. Bir de kopyalama işlemi var. Bu örnekte CC derleyiciye bırakıyorum. Derleyici ne görürse karşılıklı birbirine kopyalıyor. Bunlara “memberwise copy” yada “shallow copy” deniyor. (Deep copy bunların tersi) Deep copy ile independency yapacağız.

Compiler Default Copy Constructor

Örnekte gördüğümüz gibi bu senaryoda derleyicinin yazdığı CC işimize yaramıyor. Run time hatası alırız. Şimdi CC kendimiz yazıp bağımsızlık oluşturacağım. Şurayı çok iyi anlamamız gerekiyor, biz bir CC yazarsak derleyici bu koda kesinlikle müdahale etmez. Normal örnekte CC yazma nedenim pointer yani aslında m_len in doğrudan kopyalanmasının benim için bir sakıncası yok. Daha karmaşık bir örnekte olabilir di. 20 tane veri elemanı var diyelim ki, bir tanesi için aslında müdahale etmem gerekiyor. Ama 20 elemandan 19 karşıklıklı kopyalanması biz yapacağız.

CC Kendimiz Yazalım

CC maliyetli bir yapı. Bir nesneye başka bir nesne ile değer vermemiz. Call by value çağrısı, MyClass sınıfına geri dönen fonksiyon vs bunlar ciddi maliyetler. Senaryoyu biraz abartacak olursak, name olmasında matris olsun(matrislerde dev gibi olsun :D ). Hadi diğer kaynakları bıraktık, heapteki kaynak o kadar büyük olur ki, her CC belki onbinlerce bytelık bellek alanının birbirine kopyalanması anlamına gelir. Ama diğer taraftan da bunu yapmak zorundayım. Çünkü runtime hatası olması daha mı iyi? Yani CC bazı durumlarda maliyet artabilir. Özel bir senaryo uyduralım, kopyalama yaptığım x nesnesinin hayatının kesinlikle biteceğini biliyorsam ben kopyalama yerine o adresi devralırım. (Move semantiği -> r_value ref.)

Atama Operator Fonksiyonu

Copy Assignment Operator
Copy Assignment Operator

Atama Operator Fonksiyonu ile CC ortak parçası “deep copy”.

Destructor ile Atama Operatör Fonksiyonunun ortak noktası her ikisi de kaynakları iade ediyor.

Buradan şu ortaya çıkıyor, eğer bir sınıf için destructor yazmışsak CC yazmalıyız aynı zamanda atama operator fonksiyonunu da yazmalıyız.

C++ en meşhur terimlerinden biri “Big Three(Büyük üçlü)”.
Neye büyük üçlü deniyor?
~> destructor, copy constructor, copy assignment operator oluşturduğu üçlüye. Birimiz varsak diğerlerimizde olucak diyor :)
Kaynak: Rule of Three

Kaynakta yazıyor artık C++11 ile bu arttır. Rule of Five
destructor
copy constructor
move constructor
copy assignment operator
move assignment operator

Move semantiğini ve Perfect Forwarding gerçekleştirmek için C++11 standartlarında sağ taraf referansı diye bir araç eklendi. Eskiden referans referanstı. Şimdi önce ki referans dediklerimiz şimdi sol taraf referansı olarak anılmaya başlandı. (Lvalue reference)

iki && işareti ile belirtilen referansa rvalue(sağ taraf) referansı deniyor.
Kaynak:
Lvalue, Rvalue ..

Bir sınıfın constructor parametresi sağ taraf değeri olabilir.

Sağ taraf referansı parametreli olan taşıma semantiğini yapıcak.
Sol taraf referansı parametreli olan kopyalama semantiğini yapıcak.

Bir örnek verelim;

Rvalue & Lvalue

Evet burada hangisinin çağırıldığını derleyeci belirleyecek, function overloading kurallarına göre.

Konu da ek olarak değindiğimiz move semantiği, rvalue .. gibi konuları ayrı bir başlık altında anlatmaya çalışacağım.