The Rule of Three / Five or Six

08:16
C++11'den önce bu kural Rule of Three olarak ele alınmış ve C++11'le gelen taşıma (move) semantiği ile kurala ilişkin işlev sayısı artmıştır. Bu kural bir sınıf tanımı içinde birbiriyle ilişkili toplamda 6  özel işleve ilişkindir. Bunlar;
  1. default constructor
  2. copy constructor                        //rule of three
  3. copy assignment operator   //rule of three
  4. destructor                                   //rule of three
  5. move constructor                           //rule of five
  6. move assignment operator       //rule of five
Bu kuralla bağlantılı olarak Rule of Zero başlıklı yazıda anlattığımız gibi eğer rule of zero kuralı uygulanamıyorsa; bu yukarıda listelenen bazı fonksiyonları yazdığımız anlamına gelir ki bu noktada artık Rule of Five kuralının uygulanmasının daha doğru olacağını varsayabiliriz. Ve yazdığımız sınıfa ait nesneler kopyalanabilir olacak mı? ya da taşınabilir olacak mı? sorusunun cevabına bağlı olarak bazı fonksiyonlar tanımlamalı ya da =delete edilmesi gerekmektedir. Ayrıca rule of five kuralını uygulamazsak ciddi bir C++ kodlama eforunu gerektirecek bir yola girmemize sebep olacaktır. 

Bu eforlardan birisi bizim yazdığımız özel fonksiyonlarla birlikte derleyicinin neleri yazıp yazmayacağı ya da delete edeceği sorunsalıdır. Derleyici bu noktada bizim sınıf tanımı içerisinde yazdığımız özel fonksiyonlara göre hareket etmektedir. Aşağıda bununla ilgili kural matrisini görebiliriz.
user-declared & implicitly-declared matrisi
Yukarıdaki matristen bir kaç okuma yapmak istersek; Bir sınıfın sonlandırıcı fonksiyonu bildirilirse derleyici sınıfın taşıma fonksiyonlarıyla (move members) ilgili bildirim yapmaz bunun dışındaki tüm özel işlevlerin varsayılan edileceği matrisin 5. satırında görülebilir.
  • Car() = default;
  • Car(const Car &) = default;
  • Car &operator=(const Car &) = default;
Bir sınıfın taşıyan atama fonksiyonu bildirilirse derleyici sınıfın varsayılan kurucu fonksiyonunu ve sınıfın sonlandırıcı fonksiyonunu varsayılan yapar. Bu durumda yine sınıfın kopyalayan işlevleri derleyici tarafından delete edileceği matrisin 9. satırında görülebilir.

  • Car() = default;
  • ~Car() = default; 
  • Car(const Car &) = delete;
  • Car &operator=(const Car &) = delete;
Bir sınıfın kopyalayan kurucu fonksiyonun bildirirse derleyici sınıfın kopyalayan atama işlevini ve sonlandırıcı işlevlerini varsayılan edileceği matrisin 6. satırında görülebilir.

  • ~Car() = default;
  • Car &operator=(const Car &) = default;
Ve bu noktada derleyicinin varsayılan olarak yazıp yazmayacağı yada delete edip etmeyeceği  konusunun ne kadar karmaşık olduğu ve Rule of Five kuralının biraz da bu karmaşıklığın ve olası hataların önüne geçmek için ortaya çıktığını rahatlıkla görebilirsiniz.

Örnek - 1 : Kaynak Kod
Özetle bu kural eğer birini bildiriyorsanız bu özel fonksiyonların tamamı ile ilgili bildirimleri gerçekleştirin ve karmaşadan kurtulun sonucuna varabiliriz. Yukarıdaki örnekte tam bu kurala uygun şekilde gerçekleştirilmiştir.
Örnek - 1 : Çıktı

Note:

  • Derleyici için neler bildirim olarak niteleniyor dersek? Aşağıdaki 4 durum da kullanıcı bildirimi olarak nitelendirilmektedir.
    X() {}    //user-declared

    X();     //user-declared

    X() = default;    //user-declared

    X() = delete;    //user-declared
  • Sadece kopyalanabilen (Copyable Objects) objeler söz konusu olduğunda
    • Otomatik kaynak yönetimi kullanılıyorsa: The Rule of Zero.
    • Manuel kaynak yönetimi kullanılıyorsa: The Rule of Three.
  • Hep kopyalanabilen hem taşınabilen (Moveable Objects) objeler söz konusu olduğunda
    • Otomatik kaynak yönetimi kullanılıyorsa: The Rule of Zero.
    • Manuel kaynak yönetimi kullanılıyorsa: The Rule of Five.
  • Taşınamayan Nesneler (Unmoveable Objects
    Bir nesnenin taşınarak hayata başlanmasını istemiyorsak move constructor'ın delete edilmesi gerekmektedir.

    Örnek: Car(Car&& source) = delete;

    Bir nesneye taşıma yoluyla atama yapılmasını istemiyorsak move assignment operator'ın delete edilmesi gerekmektedir.

    Örnek: Car& operator=(Car&& rhs) = delete;

  • Kopyalanamayan Nesneler (Uncopyable Objects)
    Bir nesnenin kopyalanarak hayata başlanmasını istemiyorsak copy constructor'ın delete edilmesi gerekmektedir.

    Örnek: Car(const Car& kSource) = delete;

    Bir nesneye kopyalanarak atama yapılmasını istemiyorsak copy assignment operator'ün delete edilmesi gerekmektedir.

    Örnek: Car& operator=(const Car& kRhs) = delete;

Algorithm | any_of, all_of, none_of algoritmaları


Algoritmaların template bildirimleri
C++11 ile gelen bu üç algoritma lambdalarla birlikte çok faydalı biçimlerde kullanılabilmektedir.  Üç algoritma içinde şablondan üretilecek fonksiyonun ilk iki parametresi  kontrol edilmesi gereken aralığa ilişkin başlangıç ve bitiş itearator değerleri, üçüncü parametre ise tek parametreli bir sınayıcı (predicate) 'dir. Şimdi sırasıyla çalışma mantıklarını irdeleyelim:

Algorithm | any_of

std::any_of algoritmasıyla bir aralık (range) içindeki değerlerden herhangi birinin bir koşulu sağlayıp sağlamadığını kontrol edilmektedir. std::any_of'un geri dönüş değeri bool türündendir. Herhangi biri koşulu sağlıyorsa "true" döner.
STL Test Algoritmalarından any_of örneği
Yukarıdaki örnekte string türünden bir vector container elemanlarının içerisinden herhangi birinin "Mukhtar" elemanına eşit olup olmadığı kontrol edilmiş ve bir tanesi koşulu sağladığı için true dönmüştür. ikinci kontrolde ise herhangi bir üye'nin uzunluğu 11'den fazla mı kontrolü yapılmış ve koşul sağlandığı için true geri dönmüştür.

Algorithm | all_of

std::all_of algoritması ile bir aralık (range) içindeki tüm değerlerin bir koşulu sağlayıp sağlamadığını kontrol edilmektedir. std::all_of'un geri dönüş değeri bool türündendir. Tamamı koşulu sağlıyorsa "true" döner.
STL Test Algoritmalarından all_of örneği
Yukarıdaki örnekte string türünden bir vector container elemanlarının hepsinin uzunluğu 6'dan büyük mü olup olmadığı kontrolü yapılmış ve hepsi koşulu sağladığı için true dönmüştür. ikinci kontrolde ise hepsinin uzunluğu 11'den büyük mü olup olmadığı kontrolü yapılmış ve hepsi koşulu sağlamadığı için false dönmüştür.

Algorithm | none_of

std::none_of algoritması bir aralık (range) içindeki tüm değerlerin verilen bir koşulu sağlamadığını kontrol etmektedir. std::none_of'un geri dönüş değeri bool türündendir. Tamamı koşulu sağlamıyorsa "true" döner. Bu noktada none_of algoritmasının all_of algoritmasının lojik değilidir demek yanlış olmaz.
STL Test Algoritmalarından none_of örneği
Yukarıdaki örnekte string türünden bir vector container elemanlarının hepsinin "Aydin" elemanına eşdeğer olup olmadığı kontrol edilmiş ve bir tane bile olmadığı için true değer olmuştur. Bir tane bile koşul sağlansaydı false değer dönecekti. ikinci kontrolde ise container elemanlarının hepsinin uzunluğu 7'den küçük olmadığı kontrolü yapılmış ve hiç bir container elemanının uzunluğu 7'den küçük olmadığı için koşul sağlandığı için true geri dönmüştür.

The Rule of Zero

06:44
"The Rule of Zero" kuralı sınıfınıza ait destructor, copy constructor, move constructor, copy assignment operator ve move assignment operator fonksiyonlarının hiçbirinin doğrudan bizim tarafımızdan tanımlanmaması anlamına gelmektedir. Peki ne zaman? bu kurala uymak gerekir. Bu noktada önce iki ayrıma dikkat etmek gerekir:

  • Yukarıda bahsi geçen özel fonksiyonlardan herhangi bir fonksiyonu tanımlamaktan kaçınabiliyorsanız, kaçının. Bu sayede hiçbirinin tanımlanmasına gerek kalmıyorsa buna kısaca "Rule of Zero" denilmektedir.
  • Yukarıda bahsi geçen özel fonksiyonlardan herhangi bir fonksiyonu tanımlıyorsanız tamamını tanımlayın, delete ediyorsanız tamamını delete edin. Buna kısaca "Rule of Five" denilmektedir.
Yukarıda iki kuraldan bizi ilgilendiren kısım olan Rule of Zero için şu denilebilir ki; tanımlamaktan kaçabiliyorsanız tanımlamayın. Peki C++ için bu idiomlar neden vardır? veya arka planda neyi ilgilendiriyor? derseniz karşımıza alt seviye dillerde önemli bir sorun olan kaynak yönetimi çıkmaktadır. C++ için Garbage Collector gibi bir yapı olmadığından dolayı kaynakların yönetimi nasıl olacak? sorusuna idiomlar ile çözüm getirilmeye çalışılmıştır. Bu noktada şunu da belirtmeliyiz ki kaynak yönetimi için C'de yer alan raw pointer'lar asla kullanılmamalıdır.  Ve bir C++ programcısı perspektifinden sistemdeki kaynak yönetimi söz konusu olduğunda, nesneleri dört biçimde kategorize edebiliriz:
  • Taşınabilen ve kopyalanabilen nesneler
  • Kopyalanabilen fakat taşınamayan nesneler
  • Taşınabilen fakat kopyalanamayan nesneler
  • Ne kopyalanabilen ne de taşınabilen nesneler
Bu nesnelerin tipine göre sınıf tanımlarında aşağıdaki fonsiyonların tanımlanması ya da delete edilmesi gerekmektedir. Bu fonksiyonlar;
  • Copy constructor
  • Copy assignment operator
  • Move constructor
  • Move assignment operator
Aşağıdaki örnekte Rule of Zero kuralına uygun olarak Car sınıfı tanımlanırken hiçbir özel fonksiyon tanımlaması yapılmamıştır. Ve kaynak yönetimi doğrudan compilerın yazdığı koda havale edilmiştir.
 

Aşağıdaki örnekte sınıfımızın move/copy constructible ya da assignable olup olmadığını incelemeye çalıştık.


Bu yazı Rule of Three/Five yazısıyla devam edilecektir.

Note:

  • default constructor:   X()
  • copy constructor:   X(const X&)
  • copy assignment:   operator=(const X&)
  • move constructor:   X(X&&)
  • move assignment:   operator=(X&&)
  • destructor:   ~X()

PIMPL Idiom (PImpl idiyomu) - 1

12:12 , ,
"Pointer based IMPlementation" veya daha yaygın/kısa ismiyle PImpl bir C++ programlama tekniğidir. Neden böyle bir kullanım yöntemi geliştirilmiştir derseniz:
  • Derleme bağımlılıkları azaltarak yapılacak değişikliklerden kullanıcı kodlarını en az etkilenmesini sağlama
  • Kaynak kodun dış dünyaya açılmasını önleme
gibi iki temel ihtiyacı karşılaması içindir. Bunun için temelde bir pointer ve bir sınıf kullanılmaktadır.

PImpl bir yönüyle compiler firewall'u gibi calışmaktadır 
Bu noktada programlamaya ilişkin iki temel kavram açıklamak gerekir:
  • Başlık dosyası (Header file, .h,.hpp, ...) : hangi sınıfları/fonksiyonları dış dünyaya açtığınızı ve nasıl kullanılacağını gösterir. Dış dünyayla paylaşılması gereken dosyadır.
  • Binary dosyalar (.lib, .dll, .a, .so, ...) : .c, .cpp, .cxx, ... dosyaların derlenmiş halleri ve başlık dosyaları ile birlikte verilecek 2. dosyadır.
.h gibi başlık dosyaları 3. kişilere verilmesi gereken ve içine yazılan her şeyi karşı tarafın açık bir şekilde görebileceği dosyalardır. Bu nedenle PImpl idiyomu bize kısaca başlık dosyasına daha az kaynak kod yazmamıza (yaptığınız implementasyonun detaylarını gizlemek ve başlık dosyalarında çok sık değişiklik yapmamaya çalışmak) olanak sağlayan idiyomdur.

cripto.h başlık dosyası
Yukarıdaki kodda görüleceği üzere sınıfın dış dünyaya açtığı iki tane fonksiyon bulunmakta ve PImpl idiyomunu gerçekleştirmek için private bölüme iki temel bildirim yapılmış.

cripto.cpp kaynak kod dosyası
Yukarıdaki kaynak kodun kullanıcı tarafından görülemeyeceğini düşünürsek yaptığımız ikinci bir sınıf kodunun hiç bir şekilde (reverse edilmediğini varsayarsak :) dışarıya sızmayacağı ve cripto.cpp dosyasında yapılacak bir değişiklik için kullanıcı tarafında bir derleme işlemine gerek olmamakta, sadece yeni derlenmiş .so benzeri dosyanın paylaşılması yeterli olacaktır.

cripto.cpp kaynak kod dosyası (Devam)
Yukarıdaki kodda dikkat çekmek istediğim nokta ise constructor tanımlaması yapılırken pImpl'ye atama yapıldığının gözden kaçmamasıdır. Atama işlemini unutmanız halinde program segmentation fault ile sonlanacaktır.

main.cpp kaynak kod dosyası
Yazdığımız yukarıdaki kodda tamamen tasarımsal olarak 2 farklı şekilde nesne tanımlaması yapılabilmekte ve buna ilişkin test amaçlı yazdığımız kod görülmektedir.

Note:

  • Yardımcı olması açısından Linux için derleme parametreleri:
    g++ -std=c++11 -O2 -Wall -c cripto.cpp
    g++ -std=c++11 -O2 -Wall main.cpp cripto.o -o pimpl
    ./pimpl