Generalized Constant Expressions (Genelleştirilmiş sabit ifadeler) - 1

12:53 ,
C++11'de, yazılan programların her zamankinden daha hızlı çalışmasına olanak tanıyan bir çok özellik bulunmaktadır. Bu özelliklerden biri de genelleştirilmiş sabit ifadeler (Generalized Constant Expressions), yani yazılan programların bazı parçalarının derleme zamanında hesaplamasından yararlanılmasını sağlamaktır. Sabit ifadelerin temel fikri, belirli hesaplamaları program çalıştırıldığında değil de derleme zamanına havale etmektir. Bu yöntem, görünür bir performans avantajına sahiptir: Programın her çalıştırıldığında bazı hesaplamaların tekrar tekrar hesaplanmasına gerek kalmadan bir sefer yapıp işlem maliyetini O(1) düzeyine çekmektir. Kuralı tek bir cümleyle ifade etmek gerekirse: Derleme işlemi sırasında bir şey yapılabiliyorsa, onu derleme zamanına havale et bitsin. Örneğin bir sayının sinüs veya kosinüs değerini hesaplamanız mı gerekiyor? Bunu çalışma zamanına havale etmeden derleme zamanında bunu hesaplayan bir fonksiyon oluşturarak halletmek performans artışı demektir. Son olarak genelleştirilmiş sabit ifadeleri constexpr anahtar kelimesi kullanılarak oluşturulur.
Genelleştirilmiş sabit ifadelere ait bir fonksiyon örneği
Genelleştirilmiş sabit ifadelere ait bir fonksiyon içerisinde dikkat edilmesi gereken kısıtlamalardan biri constexpr bir fonksiyon sadece constexpr fonksiyon çağrısında bulunabilir. Recursive bir çağrı kısıtlaması yoktur. Aşağıda örnek bir uygulama görülebilir:
Recursive genelleştirilmiş sabit ifadelere ait bir fonksiyon örneği
Dizi tanımlaması yapılırken bir hesap yapılması gerektiğinde klasik fonksiyon çağrısı yapılamamakta (derleme zamanında dizinin boyutunun bilinmesi gerektiğinden) ve makrolar kullanılmaktadır. C++'ın makrolarla arası iyi olmadığını da bir kenara koyarsak bu noktada makroları devre dışı bırakıp yerine genelleştirilmiş sabit ifadeleri kullanılabilir. Aşağıda buna ilişkin bir örneği inceleyebilirsiniz:
Makro yerine genelleştirilmiş sabit ifadelere ait bir fonksiyon örneği
Genelleştirilmiş sabit ifadelerine ilişkin kullanım senaryolarından biri de nesneleri compile time sırasında kullanımına ilişkindir. Aşağıda genelleştirilmiş sabit ifadeleri kavramının olmadığı bir örneği görebiliriz:
Genelleştirilmiş sabit ifadeler ile Compile Time'da nesne kullanımı örneği -1
Peki nu noktada ben bir kare objesini derleme zamanında oluşturup alanını  derleme zamanında hesaplamak istediğimde ne yapmam gerekir? Birincisi constructor constexpr olmalı ve ikincisi çağıracağım fonksiyon constexpr olmalıdır.
Genelleştirilmiş sabit ifadeler ile Compile Time'da nesne kullanımı örneği -2

Constexpr değişkenler üzerindeki kısıtlamalar

  • Constexpr gösterici değişkenlere de sabit ifadesi olma özelliğine sahip adreslerle ilk değer verilmelidir. Bu noktada şu bilinmelidir ki, yerel değişkenlerin adresleri sabit ifadesi olarak kabul edilmemekte fakat global değişkenlerin adresleri sabit ifadesi olarak kabul edilmektedir.
  • Fonksiyonların parametre değişkenleri ve bir sınıfın static olmayan veri öğeleri constexpr olarak bildirilemezler.
  • constexpr nesneye sabit ifadeleriyle ilk değer verilmesi zorunluyken const nesne için böyle bir zorunluluk yoktur. 


Constexpr fonksiyonlar üzerindeki kısıtlamalar

  • Tek return ifadesinden oluşmalıdır (birkaç istisna dışında)
  • Yalnızca diğer constexpr fonksiyonlarını return ifadesi içinde çağırabilir.
  • constexpr anahtar kelimesinin fonksiyon tanımlamasında kullanıldığında fonksiyonu otomatik olarak inline yapar (constexpr fonksiyonlar tipik olarak başlık dosyalarında tanımlanmalıdır).

Note:

  • Derleyicinin derleme zamanında değerini bilme ya da hesaplama garantisi olan ifadelere sabit ifadesi denmektedir.
  • constexpr ile tanıtılan bir değişkene bir sabit ifadesi ile ilk değer verilmeli  ve derleyici bu kontrolü yapmakla yükümlüdür. constexpr bir değişkene ilk değer verilmemesi ya da sabit ifadesi olmayan bir ifade ile ilk değer verilmesi doğrudan kural hatasıdır.
  • constexpr anahtar sözcüğü hem bir değişkenin hem de bir fonksiyonun tanımında  kullanılabilmektedir.
  • constexpr bir değişken ifadesi bir dizi tanımında boyut ifadesi olarak kullanılabilir. Bir numaralandırma (enumaration) değeri olarak kullanılabilir. Bir switch deyiminin case ifadesi olabilir. Sabit parametreli (nontype template parameter) bir şablonun açılımında arguman olarak kullanılabilir.

Smart Pointers | unique_ptr (unique_ptr akıllı göstericiler) - 1

Pointer (Raw Pointer) 'lar hafızada belirli bir alanın (genelde dinamik bir alanın) adresini gösterir ve ilgili alan ile programcının işi bittiğinde bu alanın sisteme iade edilmesi programcıya bırakılmıştır. C dilinde bunun doğal şekli budur ve kontrol tamamen programcıya aittir. C++ 'da bulunan smart pointer 'lar sayesinde ise dinamik alanın iade edilmesi otomatik olarak uygun bir zamanda gerçekleştirilir. Bu da programcının gözünden kaçabilecek hataların önlenmesi anlamına gelmektedir. Yazımızın konusu olan unique_ptr ise; C++11 standartları ile birlikte gelen akıllı gösterici (smart pointer) türü sınıfındandır. unique_ptr sınıfı C++98 standartlarında var olan tasarımsal olarak çok kullanışlı olmayan auto_ptr sınıfının yerine getirilmiştir. auto_ptr 'de bulunan ve hataya sebep olabilecek birden fazla sahiplik özelliği yoktur, tek bir sahiplik (exclusive ownership) vardır. Bir unique_ptr nesnesi, kendi hayatı sona erince işaret ettiği dinamik alanı da iade ettiğinden kaynakların sisteme iade edilmesini sağlar. Copy constructor, operator= fonksiyonları delete edildiğinden dolayısıyla kullanıcı herhangi bir yerde herhangi bir şekilde nesneyi kopyalayamayacaktır. Bu noktada sorumluluğu programcıya bırakılan 2 noktayı yazının devamında açıklayacağız. Son olarak C++11 standartları ile auto_ptr sınıfı kullanımdan düşürülmüş (deprecated)' tür. 
Bir programcının bakış açısında fonksiyonlar işlerini yaparken gerçekleştirdikleri adımlar kabaca şu şekildedir:
  • Önce işlerini gerçekleştirebilmek için bazı kaynaklar edinirler veya var olan kaynaklarının adresleri aktarılır
  • Sorumlusu oldukları işleri gerçekleştirirler
  • İşlerini tamamladıktan sonra kendi edindikleri/sorumlusu oldukları kaynakları sisteme geri verirler. Bu noktada şu durumu da göz önünde bulunduralım.  İşlerini tamamlayamadan bir hata durumunda bu kaynakların akıbeti ne olacak? Raw pointerlarla çalışıldığında tüm sorumluluk programcıdadır.
Şimdi unique_ptr bize yukarıda kaynaklar/hata durumundaki senaryolara karşı sağladıklarını ve genel özelliklerini inceleyelim.
Aşağıdaki örnekte bize bir unique_ptr nesnesinin değer yoluyla aktarılamıyacağının altı çizilmiş. Çünkü unique_ptr nesnesinin copy constructor'ı bulunmamaktadır. Peki değer yoluyla katarım yapılsaydı ne mi olurdu? func fonksiyonun ptr parametresi uptr'nin işaret ettiği Bird nesnesini işaret edecekti ve fonksiyonun işi bittiğinde ptr değişkeninin ömrü de biteceğinden işaret ettiği kaynağı sisteme iade edecek ve bunun sonucu olarak da programın devamında hatayla karşılaşılacaktı.


Bu durumda karşımıza 2 seçenek çıkmaktadır ya referans yoluyla aktarım ya da ileride görebileceğimiz std::move ile sahipliğini başka bir unique_ptr 'ye geçirmek. Biz aşağıdaki örnekte referans yoluyla aktarım yaparak programın çalışabilir hale gelmesini sağladık.



Bir unique_ptr nesnesinin işaret ettiği bir nesne var mı yok mu sorusuna cevap olabilecek 3 farklı yöntem mevcuttur. Bunlar:
  • <unique_ptr değişkeni>
  • <unique_ptr değişkeni> == nullptr
  • <unique_ptr değişkeni>.get() == nullptr


Yukarıda görülebileceği üzere uptr nesnesi hiç bir yeri işaret etmediği için üç durumda da if deyimi içerisine girmiştir. Aşağıdaki örnekte ise aynı testi bir değeri işaret ettiğinde uygularsak;


<unique_ptr değişkeni>.release() fonksiyonu ise sorumluluğunu aldığı dinamik nesneyi bırakır ve geri dönüş değeri olarak bu nesnenin adresini geri döner. Artık bu noktadan sonra bu yerle ilgili sorumluluk smart pointer'dan çıkmış programcıya geçmiştir. Bu noktadan sonra unique_ptr değişkeni hiçbir yeri işaret etmemektedir. Aşağıda bu durumla ilgili örnek incelenebilir.


unique_ptr sınıfı tek sahiplik semantiğini uygular. Başta söylediğimiz sorumluluğu programcıya bırakılan 2 noktadan birincisi: birden fazla unique_ptr nesnesinin aynı dinamik nesnenin adresiyle başlatılıp/başlatılmaması programcının sorumluluğundadır.


Yukarıdaki örnekte iki unique_ptr nesnesi aynı yeri göstermiş ve program sonlanırken birinci unique_ptr kaynakları iade etmiş ve ikincide aynı adresi iade etmeye çalıştığı için program çalışma anında hatayla karşılaşmıştır. Böyle bir durumda sorumluluk yukarıda da dediğimiz gibi tamamen programcının sorumluluğundadır.
Bir unique_ptr nesnesini kopyalama semantiğiyle hayata başlatamamayız ve bir unique_ptr nesnesine kopyalama semantiğiyle atama yapamayız. unique_ptr sınıfında taşıma semantiği kullanılarak atama veya ilk değer verme işlemlerini gerçekleştirebiliriz. Aşağıdaki örnekte kopyalama ve taşıma ile ilk değer verilmeye çalışılmış ve kopyalama ile yapılamayacağına ilişkin noktanın altı çizilmiştir.


Bir unique_ptr nesnesine ait sahipliğin std::move sahipliğinin devredildiğini aşağıdaki örnekte görebilirsiniz. uptr1 artık hiçbir yeri göstermemektedir.


Aşağıdaki örnekte ise bir unique_ptr nesnesine  atama ile ilk değer verilmesini görebiliriz.


Bir unique_ptr nesnesine  std::move ile atama yapılması durumunda daha önce gösterdiği bir yer varsa ona ait kaynaklar iade edilmekte daha sonra yeni alanın sorumluluğu alınmaktadır. Aşağıdaki örnekte uptr2'nin gösterdiği alan iade edilmiş ve destructor'ın çağrıldığı ve ardından uptr1'in gösterdiği alanın sorumluluğu uptr2'ye geçtiğini görebiliriz.


 Bir unique_ptr nesnesine ait reset() fonksiyonunu çağırmak işaret ettiği kaynakların iade edilmesi anlamına gelmektedir. Aşağıda buna ilişkin örneği görebilirsiniz.


Bir unique_ptr nesnesine nullptr değerinin atanması nesnenin reset() fonksiyonunun çağrılmasına eşdeğerdir. Aşağıda buna ilişkin örneği görebilirsiniz.


 Bir unique_ptr nesnesine ait release fonksiyonunu çağırmak işaret ettiği kaynakların iade edileceği anlamına gelmemektedir. Sadece sorumluluğun bırakılması anlamına gelmektedir. Aşağıdaki örnekte destructor'ın çağrılmadığını görebiliriz.





R-Value References (Sağ Taraf Referansları) - 1

Sağ taraf referansları (r-value references) C++11 standartlarıyla C++ diline eklenmiş en önemli araçlardan biridir. Fakat kavram biraz karmaşık bir şekilde standarlaştırılmış ve bu durum insanların anlamasını fazlasıyla zorlaştırmaktadır. Şimdi bu konuyu anlaşılır hale getirmeye çalışalım.
tüm referans ağacı

Basitleştirilmiş bir tanım yapmak gerekirse; Atama işlecinin hem sol tarafında hem sağ tarafında yer alabilen ifadeler sol taraf değeridir. Atama işlecinin yalnızca sağ tarafında yer alabilen ifadeler sağ taraf değeridir. Daha kısa bir tanım yapmak geekirse bellekte bir yer tahsis edilmiş değerlere sol  taraf değeri; bellekte bir yer tahsis edilmemiş değerler ise sağ taraf değeridir

R-Value References (Sağ Taraf Referansları)

T bir tür olmak üzere, sağ taraf referansı olarak T&& şeklinde, sol taraf referansı olarak T& şeklinde ifade edilecektir. Bir sağ taraf referansı birkaç istisna dışında sol taraf referanslar (T&) gibidir. Ayrımın olduğu noktalardan biri ise sol taraf referans ve sağ taraf referans paremetrelerine sahip aynı isimli fonksiyonlar bir arada (overload) bulunduğunda sol taraf değerleri için sol taraf referansa sahip parametreli fonksiyon, sağ taraf değerleri için sağ taraf referansa sahip parametreli fonksiyon çağrılır.

sağ taraf ve sol taraf referanslara ilişkin overload örneği

  1. void func(int &x);       //sol taraf referansı parametreli
  2. void func(int &&x);    //sağ taraf referans parametreli
Yukarıda kod 
  • func(69); şeklinde bir çağrımda 69 bir sağ değer olduğu için 2 numaralı fonksiyon
  • int i = 69;
    func(i); şeklinde bir çağrımda i bir sol değer olduğu için 1 numaralı fonksiyon
Öncelikle normal bir referansa (sol taraf değerine bağlanan referans) verilen ilk değerin aynı türden nesne belirten bir ifade (yani sol taraf değeri)  olması gerektiğini aşağıdaki örnek kodla anımsayalım:
sol taraf referanslara ilişkin atama örneği
Sol taraf referanslı bir değere doğrudan sağ taraf referanslı bir değer atanamayacağına dair hata kodu aşağıdaki gibidir: 


Bir sağ değer referans'a verilen ilk değerin aynı türden nesne belirten bir ifade yani sağ değer olması gerektiğini aşağıdaki örnek kodla anımsayalım:
sağ taraf referanslara ilişkin atama örneği
Sağ değer referans'a ilk değerin doğrudan sol taraf değer atanamayacağına dair hata kodu aşağıdaki gibidir: 

Sağ taraf değeri belirten bazı ifadeler;

  • Sabitler
    Örneğin:
    2, 3,1, vb. //Sağ taraf değeri
  • Çeşitli aritmetik operatörlerle oluşturulan ifadeler 
    Örneğin:
    a+b/c, vb.    //Sağ taraf değeri
  • Geri dönüş değeri referans olmayan fonksiyon çağrıları
    Örneğin:
    int foo();
    int &&bar();
    yukarıdaki prototipleri görünen foo(), bar(); fonksiyonlarını çağıran ifadeler //Sağ taraf değeri
  • Geçici nesneler
    Örneğin:
    class Foo {
     public:
      //...
    };

    ...
    Foo(); //Sağ taraf değeri
Sağ taraf referansları başlıca iki soruna çözüm getirmek için ortaya çıkan bir kavramdır. Bunlar;
  • Taşıma semantiğinin (move semantics) gerçekleştirilmesi
  • Mükemmel gönderim (perfect forwarding)
Bir sonraki bölümde bu iki kavramı detaylı anlatıp Sağ Taraf Referansları (R-Value References) kavramını daha anlaşılır hale getirmeye çalışacağız.

Not 

  • Dekleratördeki && atomlarını ** atomlarına benzeterek referans referansları varsayımında bulunmak tamamen hatalıdır. Bu yapay bir notasyondur.
  • double dval = 3.2;
    const int &ival = dval;
    bir atamada aslında olan:
    double dval = 3.2;
    const int itmp = dval;
    const int &ival = itmp;
  • Sol taraf değerine bağlanan referanslara verilen ilk değerlerin nesne belirten ifadeler olması zorunludur. Fakat sağ taraf değerine bağlanan referanslara verilen ilk değerler için nesne belirtme zorunluluğu yoktur.
    int &&val = foo();
    int &val2 = 5; //Geçersiz


Lambda Expression (Lambda ifadeleri) - 1

C++11’le gelen ve yaygın biçimde kullanılan özelliklerden biri de lambda'lardır. Lambda ifadeleri, bir fonksiyon çağrısının yapılacağı yerde doğrudan fonksiyonun kodunu yazabilme olanağını sağlar. Bir lambda ifadesi çağrılabilecek (callable) bir kod birimine karşılık gelir ve derleyici lambda ifade için kendi oluşturduğu bir sınıf türünden bir geçici nesne oluşturacak şekilde kod üretir. Derleyicinin lambda ifadesi karşılığı oluşturduğu sınıf, "closure type" türünden, lambda ifadesi ile oluşturulan geçici nesne de “closure” ya da “closure object” olarak isimlendirilir.

Lambda ifadesinin elemanları


Temel Lambda Sentaksı

   auto l = []() { return "Hello Lambda!..\n"; };

Yukarıdaki kod parçasında auto belirteci kullanılarak l değişkenine lambda ifadesi ile oluşturulan geçici nesne ile ilk değer verilmektedir. Bu durumda l değişkeni derleyicinin lambda ifadesi karşılığında oluşturacağı sınıfın türündendir. Bir lambda ifadesini formülize etmek gerekirse:

[yakalama listesi] (parametre listesi)   ->geri dönüş türü  {lambda ifadesinin gerçekleştireceği kod}

biçiminde ifade edebiliriz. Lambda ifadeleri iki farklı biçimde çağırabiliriz;
  • Fonksiyon çağrı operatörü ile:   l(); 
  • Doğrudan tanımlamanın yapıldığı yerde:  []() { return "Hello Lambda!..\n"; }();

Bir lambda ifadesinin tanımlanması ve çağrılmasına örnek

Lambda'ların Yakalama (Capture Specification) Listesi

Bir lambda ifadesi içinde, o ifadeyi kapsayan bloklarda yer alan dışsal değişkenler özel bir sentaks ile kullanılabilir. Köşeli parantez içerisine yazılan bu sentaks sayesinde kopyalama ya da referans semantiği ile lambda ifadesi içinde kullanılabilir:

Kopyalama yoluyla yakalama [=]

Bu durumda kullanılan tüm dış değişkenler varsayılan şekilde kopyalama ile yakalanır. Her bir değişkenin isminin tek tek yazılmasına gerek kalmaz.

Referans yoluyla yakalama [&]

Bu durumda kullanılan tüm dış değişkenler referans yoluyla yakalanır. Yine her bir değişkenin isminin tek tek yazılmasına gerek kalmaz.
  • [=, &x] : Buradaki = karakteri varsayılan yakalama biçiminin kopyalama olduğunu ifade eder. Bu, x dışında kullanılan tüm isimlerin kopyalama yoluyla yakalanacağı yalnızca x değişkeninin referans yoluyla yakalanacağı anlamındadır.
  • [&, y] : Buradaki & karakteri varsayılan yakalama biçiminin referans olduğunu ifade eder. Bu, y dışında kullanılan tüm isimlerin referans yoluyla yakalanacağı yalnızca y değişkeninin kopyalama yoluyla yakalanacağı anlamına geliyor.
  • [&x, y] : Buradaki x değişkeninin referans yoluyla y değişkeninin kopyalama yoluyla yakalanacağını ifade eder.
Kopyalama ve referans yoluyla yakalama biçimini ele alan bir örnek

Lambda'ların Parametre (Lambda Declarator) Listesi

Parantez içinde tanımladığımız değişkenler derleyicinin yazacağı fonkiyonun parametre değişkenleridir. Lambda ifadesi karşılığı oluşturulacak fonksiyonun parametre değişkeni yok ise parametre parantezi yazılmayabilir.

Lambda ifadesine referans türünde parametre bildirimine bir örnek

Lambda'ların Geri Dönüş Türleri

Derleyici bir lambda ifadesinin geri dönüş değerinin türünü doğrudan belirtilmemmiş ise lambda ifadesinde kullanılan return ifadesinin türünü kabul eder. Yani derleyici lambda ifadesinin geri dönüş değerinin türünün ne olduğunu return ifadesinden bir çıkarım yaparak anlar. Lambda ifadesinde yer alan blok içinde yazılan kodda bir return deyimi yok ise lambda'nın geri dönüş değerinin türü void kabul edilir.
  • [](int x) {return x;}ifadesiyle oluşturulacak lambda ifadesinin geri dönüş değerinin türü int'dir
  • [](int x) ->double {return x;} ifadesiyle oluşturulacak lambda ifadesinin geri dönüş değerinin türü x ifadesinin türü olan int değil doğrudan belirtilen double türündendir. 
Bu noktada lambda ifadelerde doğrudan tür belirtmek için -> syntax'ı kullanılır. Lambda ifadesinin geri dönüş değerinin türünün parametre parantezini izleyen ok (->) atomundan sonra yazıldığını (trailing return type) görebiliriz. Eğer lambda ifadesinin kodu tek bir return deyiminden oluşmuyor ise geri dönüş değeri türünün açıkça yazılması zorunludur. Aksi halde lambda ifadesinin geri dönüş türü void kabul edilir. Ve kod geçersiz duruma düşer.

Lambda ifadesinin constructor'a parametre olarak geçirilmesine bir örnek

Lambda ve STL

Lambda'ların STL kütüphanesinde bulunan fonksiyonlarla kullanımı oldukça pratiktir. Bu nedenle lambda'ların en sık kullanıldığı yer STL algoritmalarına yapılan çağrılardır diyebiliriz. 
Lambda ifadesinin STL algoritmalarıyla birlikte kullanılmasına örnek
Yukarıdaki kodda for_each algoritması birinci ve ikinci parametrelerde Iterator, üçüncü parametrede ise function object istemektedir.

for_each algoritmasının implimantasyonuna bir örnek

Bu noktada biz function objesi olarak lambda ifadesini doğrudan kullanabildik.

Notlar

  • Lambda ifadeleri tamamen aynı olsa bile, derleyici tarafından oluşturulan sınıflar birbirinden farklıdır. Benzer türdendir diyemeyiz.
  • []() -> std::string { return "Hello Lambda!..\n"; }; gibi bir ifadeyi görünce derleyici
class tempclass {
public:
std::string operator()() const
{
return "Hello Lambda!..\n";
}
}; yukarıdaki gibi bir sınıfa dönüştürdüğünü varsayabiliriz.