C++11 – std::mem_fn, std::bind, std::function

C veya C++ programlama dilinde parametre olarak bir fonksiyon vermek istediğimizde veya bu fonksiyonu bir değişkene atamak istediğimizde ihtiyacımız olan özellik fonksiyon göstericileriydi (function pointers). Ama göstericiler zaten insanları korkuturken fonk. göstericilerinin o heybetli tanımı iyice korkutucuydu. 🙂

Modern C++ fonksiyon göstericileri getirdiği sınıflar ile daha modern ve kullanışlı bir hale getiriyor.

Aşağıda C stili bir fonksiyon gösterici tanımı ve çağrılması var.

#include <iostream>


int add(int a, int b)
{
    
    return a + b;
}


using namespace std;


int main(int argc, char* argv[])
{
    int (*f)(int, int) = add;
     //auto f = add; 
    
    cout << f(5, 6) << endl;
}

Aslında C++11 ile gelen özelliklerden biri olan auto anahtar sözcüğü ile bu zahmetli tanımdan kurtulabiliriz (yorum satırında örnek kullanımı görebilirsiniz). Ya da bir fonksiyona parametre olarak verilip callback olarak kullanılacaksa bu “türü” typedef anahtar sözcüğü ile daha anlaşılır hale getirebiliriz;

#include <iostream>


typedef int(*callback_add_t)(int, int);

int add(int a, int b)
{
    
    return a + b;
}


int foo(int a, int b, callback_add_t func)
{
   return  func(a, b);
}

using namespace std;


int main(int argc, char* argv[])
{
    cout << foo(5, 6, add) << endl;
}

Bu yazıda bu yaklaşımdan ziyade C++11 ile gelen fonksiyonlar üzerinde programcıyla mutlak hakimiyet sağlayan std::mem_fn, std::bind, std::function şablon sınıfları ve fonksiyonlarından bahsetmek istiyorum.

std::mem_fn:
Üye fonksiyonları sarmalayan bir fonksiyon şablon sınıfıdır. Sınıf üye fonksiyonlarını çağırmak için kullanılır.

#include <iostream>
#include <functional>


using namespace std;


struct SDummy
{
    void who_am_i()
    {
        cout << "Jackie Chan" << endl;
    }
    
    void how_old_am_i(int i)
    {
        cout << "I am " << i << "years old" << endl;
    }
    
};

int main(int argc, char** argv) 
{
    struct SDummy dummy;
    auto jchan = std::mem_fn(&SDummy::who_am_i);
    
    jchan(dummy);


    return 0;
}

mem_fn sınıfını kullanmak yerine member function pointer kullansaydık şöyle bir tanım yapmamız gerekecekti;

void (SDummy::*pMemberFunc)();

Bu fonksiyon göstericisini bir fonksiyona parametre olarak geçip kullanmak istiyorsaniz ait oldugu nesneyi de o fonksiyona parametre olarak vermeniz gerekmektedir. Ama daha kolay yollari var 🙂 .

std::bind

Adı üstünde aslında bir fonksiyonun istenildiği sekilde çağrılma kalıbına uygun bir obje oluşturarak saklamanızı sağlayan şablon sınıfıdır. Dilerseniz bu objeyi std::function sablon sınıfını da kullanarak rahatlıkla istenilen yere taşıyabilirsiniz.

#include <iostream>


using namespace std;


struct SDummy
{
    
    int operator()(int a, int b, int c, int d)
    {
        return std::max(std::initializer_list<int> {a, b, c, d}); 
    }
    
    
};

bool isGreaterThen(int a, int b)
{
    return a < b;
}

int main(int argc, char** argv) 
{
    SDummy s;
    auto f = std::bind(s, placeholders::_1, 0, placeholders::_2, placeholders::_3);
    
    cout << f(1, 2, 3) << endl;
    
    
    auto isGreaterThenFive = std::bind(isGreaterThen, 5, placeholders::_1);
    
    cout.setf(ios_base::boolalpha);
    cout << isGreaterThenFive(3) << endl;

    return 0;
}

Örneğimizde gördüğünüz gibi bir çağırılabilen bir nesneyi çeşitli parametrelerle ‘bind’ ediyoruz. ‘placeholders::n’ lar aslında bizde ‘bind’ edilen nesneye daha sonrasında, çağrılma esnasında çeşitli parametrelere çağırma esnekliği sunuyorlar. Dikkat ederseniz std::bind şablon sınıfının geri dönüş değerini kısmen gizledik aslında o bir std::function şablon sınıfı türünden bir nesne.

std::function

Evet aslında function şablon sınıfının kullanimi size fonksiyon göstericilerini hatırlatacak. Kısaca function şablon sınııi imzasını verdiğiniz fonksiyonları obje olarak muhafaza eder.

#include <iostream>


using namespace std;


struct SDummy
{
    
    void print(int a, int b, int c)
    {
        cout << "I am Dummy Struct " << a << ", "<< b << ", " << c << endl;
    }
    
    int operator()(int a, int b, int c, int d)
    {
        return std::max(std::initializer_list<int> {a, b, c, d}); 
    }
    
    
};

bool isGreaterThen(int a, int b)
{
    return a > b;
}

int main(int argc, char** argv) 
{
    std::function<bool(int, int)> f = isGreaterThen; //1
    
    cout << f (3, 5) << endl;
    
    std::function<void()> f_lambda = []() { cout << "I am lambda" << endl; };//2
    
    f_lambda();

    SDummy s;
    std::function<int(int, int, int)> maximumOne = std::bind(s, placeholders::_1, 0, placeholders::_2, placeholders::_3); //3
    
    cout << maximumOne(1, 2, 3) << endl;
        
    std::function<void(int, int)> dummyPrint = std::bind(&SDummy::print, s, placeholders::_2, 0, placeholders::_1); //4 
    
    dummyPrint(4, 5);
    
    return 0;
}

Görüldüğü üzere std::function içerisinde fonksiyonları rahatlıkla muhafaza edebiliyoruz. Örneklerde yer vermedim ama bu nesnelerin rahatlıkla parametre olarak bir başka fonksiyona veya sınıfa verllebileceği de aşikar. Onunla ilgili örnekleri de size bırakıyorum.

1′ nolu örnekte görüldüğü üzere bool(int, int) imzalı fonksiyonu direkt fonksiyon objesi içinde muhafaza edebiliyoruz. ‘2’ nolu örnekte ise yine modern cpp özelliklerinden bir lambda fonksiyonunun uygulamasını görüyoruz. ‘3’ nolu örnekte ise ‘bind’ ile özelleştirilen bir fonksiyonunun std::function içerisinde tutulmasını ve daha sonra çağrılmasını görebilirsiniz. Bu kullanım en yaygın kullanım şeklidir. ‘4’ nolu kullanımda ise bir sınıf üye fonksiyonunun std::function şablon sınıfı ile nasıl muhafaza edilebileceğini görüyoruz. Burda dikkat edilirse ikinci parametre olarak üyesi olunan sınıf türünden bir nesne parametre olarak geçiliyor. Eh geçilmesi gerekiyor ki fonksiyon çağırılabilsin tabi üye fonksiyon static değil ise.

std::end(story)

Neden C++ öğrenmeliyim ?

Bu minvalde bir yazı yazmayı uzun zamandır düşünüyordum ki, Krzysztof Szatan’ ın bloğunda bu konuyla alakalı çok iyi bir şekilde ele alınan bir blog yazısı gördüm. Kendisinden izin alarak yazısını türkçeye çeviriyorum:

“C++ artık ölü mü ?”

Bunu 10 yıldır üniversiteye gittiğim günden beri duyuyorum. Java şu anda zaten enterprise yazılımlarda tercih edilen programlama dili, en azından ABD için bunu söyleyebilirim. Eğer programcıysanız az çok Joel Spolsky’ nin JavaSchools undan haberiniz vardır. Bu 2005 aralığındaydı. 2000 de ortaya çıkan C# ise Microsoftun Javaya bir cevabıydı aynı zamanda C++ ı bitirecek olan programlama dili olarak lanse edildi. Daha sonra dinamik programlama dönemi başladı. Ruby, Python popüler olmaya başladı. Bu dillerler beraber akla gelen sorular bu kadar kolay programlama imkanı varken neden herşey için C++’ ı kullanasın ki ? C++ sadece daha hoş bir C değil miydi ? Kaldı ki C de daha hoş bir assembly diliydi. Eğer bir saatiniz varsa Herb Sutter’ ın muhteşem konuşmasına ayırabilirsiniz. TIOBE endeksine baktığımızda son 15 yılda C++ ın 3. sırayı işgal ettiğini görüyoruz, ne dersiniz eski bir dil için hiçte fena değil bu derece bize farklı kapılar aralıyor…

Göstericileri (pointer) kim sever ki ?

Çoğu insan C++ dilini göstericiler ile özdeşleştirerek hata yapıyorlar. Benimde bu konuda kötü tecrübelerim var, çok boyutlu dizilerin adreslerinin fonksiyonlara parametre olarak verilmesi, gösterici aritmetiği, iki yönlü bağlı liste (doubly-linked list) bunların hepsi “C with classes” döneminden kalma. Belki de C++ ı bu şekilde öğretmek yanlış değildir. O halde tekrar düşün.

Tabiki profesyonel bir programcı göstericilerin (pointer) nasıl işlediğini bilmeli. Fakat bu modern C++’ a ait bir yaklaşım değil. C++ sürekli gelişiyor dün iyi bilinen bir çok şey artık değiştirilmesi gereken kötü kodlar haline geliyor.

cpptimeline

Aslında bir bakıma dil revizyon sürecinden geçiyor. Bjarne Stroustrup’ a göre: “C++11 yeni bir dil hissi veriyor, parçalar yerine oturuyor, yüksek seviyeli programlama dille eskisinden daha iyi kaynaşıyor ve daha etkili hale geliyor”. C++ yeni gelen standartlarla beraber lambdalar, memory mode, aralık tabanlı döngüler, sağ değer referansları, değişken parametreli şablonlar v.s ile birçok görevi daha yapılabilir hale getiriyor. Diyelim ki eskiden kalma kodların var ve bunların yeni standartlara göre elden geçmesi lazım. Eski kodların yeni standartlara nasıl dönüştürüleceğini söyleyen statik kod analizi yapan ve öneriler sunan araçlar hizmetinde. Bu tarz araçların C++ da yokluğu her zaman sorun olmuştur, sadece Java nın mı otomatik refactor işlemi sunduğunu sanıyorsun ? Google’dan LLVM/clang gurusu Chandler Carruth a kulak verelim, bize 100 milyon satır C++ kodunu nasıl tek seferde refactor ettiğini anlatsın.

Daha Önce Yazılmış Kodlar Gerçeği

Birçok bilgisayar mühendisliği öğrencisi kariyerlerinin başlangıcından itibaren süper kahraman gibi kod yazacaklarını düşler. Fakat genelde 20 yıllık kodlarla karşılaşır. Sabit buffer ile kullanılmış onlarca kaynak dosyaya dağılmış yüzlerce ‘strcpy’, binlerce satır uzunluktaki fonksiyonlar, 5 farklı versiyonu bulunan bağlı liste implementasyonları işte temizlemen gereken birkaç problem. “İştahını sonraya sakla junior, dün çıkması gereken release daha çıkmadı GCC nin yeni versiyonuna geçemeyiz, diğer takım buna hazır değil” diyen yöneticiler. Tam bu noktada “nerede hata yaptım” sorusu akla gelebilir.

Bu problem C++ diline özel değil. Javada, PHP de hatta Ruby ve Python da bu tarz işlerle karşılaşacaksın. Aslında bu bir çok şirketin acı gerçeği. İşte tam bu noktada söyleyeceğim şey, her programlama işinde business tarafta az vakit harcayarak eğleneceğiniz ve zorluklarla savaşacağınız işler vardır. İşte tam buralarda C++ parlıyor.

C++’ın Kral olduğu alanlar: Oyun, Finans, HPC, Derleyiciler

Eğer hayatının tamamında CRUD implemente etmek istemeyen biriysen mutlaka C++ öğrenmelisin. İşte önerilerim;

Oyun endüstrisi :Neredeyse tüm AAA oyunları C++ ile geliştirilmiş. Yazılım dünyasında da oyun geliştiricilik saygı duyulan ve en çok talep edilen alanlardan biri. Performans çok önemli dolayısıyla etkin kod yazmak çok önemli. C++ da derinleşmekten çok oyun endüstrisinde işin teoriğini öğrenmek, semantiğini kavramak ve birtakım API veya kütüphaneleri öğrenmek daha önemli. Matematik alt yapısı ve grafik bilgisi bu alan için gerekli. Bu endüstri içinde tüm kariyeriniz boyunca öğrenecek bol bol materyal mevcut.

Yüksek performanslı uygulamalar – HPC: Bu tarz uygulamalar yüksek hızlarda olabildiğince fazla işlem yapmayı gerektirir. Donanım bilgisine sahip olmanız gerekir. Ve bu bilgiyi direkt dile uygulayabilmeniz gerekmektedir. Paralel programlama kütüphanesi , CUDA ve OpenCL bu denli bir uzmanlığın C++ ile ortaya konan ürünleri. Eğer bilimsel hesaplamaya meraklı isen bu tarz bir çok iş var bu aralar, hiç makina öğrenmesini duydun mu ? Bu ara baya popüler.

Derleyiciler: Bu alan benim favorim. LLVM projesi büyük bir başarıya sahip şöyle ki LLVM nin tezgahından geçmemiş veya kütüphanelerini kullanmamış popüler bir programlama dili bulamazsınız. Proje tamamen C++. Derleyiciler bir çok programlama diliyle yazılsa da teori implementasyonun gerisinde kalıyor tıpkı diğer durumlardaki gibi. İlgilenmen gereken alanlar optimizasyon, statik analiz, debugging, standart kütüphaneler, linker ve birtakım teorik mühendislik problemleri.

Finans: Eğer finans alanında bilgi sahibi iseniz, gerçek zamanlı gecikme kabul etmeyen birçok işin olduğundan haberdarsınızdır. Matematik bilgisi bu alan için çok büyük bir artı. Bu işlerin birçoğu yüksek frekanslı alım-satım platformlarını geliştirmek ve bakımlarını yapmak diyebiliriz. Aslında bu çok güzel bir alan eğer kendinizi programcı olarak tanımlamıyorsanız.5. Bu tarz işlerin ciddi maddi getiri ve büyük faydalar sağladığını da belirtmek isterim.

Bunlar çalışma alanı önerilerinden birkaçı. Bu alanların ortak özelliği birkaç tane java çaylağını bu alanlarda çalıştırıp işi yapmasını bekleyemezsiniz. Alan bilgisinin yoğun olduğu ve tecrübenin zorunlu olduğu alanlar bunlar. Yani bu da kolay kolay vazgeçilebilecek insanlar değiller. Şirketler bu tarz insanları elde tutmak için daha çok maaş ödeyecek ve çözmeleri için ilginç problemleri onlara havale edecek. Tabi ki her işte biraz maraz olur ama gerçekten değer verdiğin ve sevdiğin işlerle uğraşıyorsan bunları görmezden gelebiliyorsun.

C++11 – std::mutex

Multi-process programlamada her bir processin kendine ait bir bellek alanı (memory space) vardır. Her bir process kendisine lazım olan veriyi kendisi elde eder. Herhangi bir veri paylaşımı gerektiğinde ise inter-process communication araçlarından birini (pipe, shared memory v.s) tercih ederek gerekli veri processler arasında dağıtılmış olur.

Multi-thread programlamada ise tüm threadlar aynı bellek alanını paylaşırlar. Bu paylaşım verinin değiştirilmesi veya okunması sırasından bir senkronizasyonu zorunlu kılar. Bunun temel amacı yarış durumundan (race condition) korunmaktır. Bu noktada programcıların en sık başvurduğu metod ise mutual exclusion yani mutexlerdir.

Şimdi aşağıdaki programa bir göz atalım.

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>


std::vector<int> ivec;
 
void addToVector(int i)
{
    ivec.push_back(i);
}

int main()
{
    std::vector<std::thread> tvec;

    for (int i = 0; i < 10; i++) {
        tvec.push_back(std::thread(addToVector, i));
    }
    
    std::for_each(tvec.begin(), tvec.end(), [](std::thread &th) {th.join();});

    
    for (auto i : ivec) 
        std::cout << i << std::endl;
    
    return 0;
}

Kod parçası 10 thread oluşturuyor ve bu threadi sırasıyla kendisine geçilen parametre değerleriyle beraber çağırıyor. Bu programın çıktı olarak ne vermesini beklersiniz ?

Herhangi bir çıktıya sahip olabilir misiniz, hatta programınız seg fault alarak da sonlanabilir veya hiçbir şey olmadan ilk bakista beklenildiği gibi,

0
1
2
3
4
5
6
7
8
9

şeklinde bir çıktı da alabilirsiniz. Kısacası bu durum bilinmez veya kestirilemez (undefined behaviour). Peki burdaki yarış durumu nerde oluşmaktadır, kısacası kodun hangi bölümünde threadler için kritik ve mutex ile koruma altına alınması gerekir ?

Cevap aslında threadler nerede ortak alana müdahale ediyorlarsa orada mutex kullanmamız lazımç Hepimizin az çok tahmin ettiği gibi threadlerin oluşturulup ivec vectorune threadler veri eklerken yarış durumu oluşur. Eğer bu bölgede mutexler vasıtasıyla koruma sağlanırsa bilinmezlik durumu ortadan kalkar ve threadlerin yarış durumuna girmeden herbirinin güvenli bir şekilde çağrılması garanti altına alınmış olur. Aşağıdaki örnek kodu inceleyelim;

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>

std::vector<int> ivec;
std::mutex mtx;
 
void addToVector(int i)
{
    mtx.lock();
    ivec.push_back(i);
    mtx.unlock();
}

int main()
{
    std::vector<std::thread> tvec;

    for (int i = 0; i < 10; i++) {
        tvec.push_back(std::thread(addToVector, i));
    }
    
    std::for_each(tvec.begin(), tvec.end(), [](std::thread &th) {th.join();});

    
    for (auto i : ivec) 
        std::cout << i << std::endl;
    
    return 0;
}

lock() ve unlock() üye fonksiyonları, aralarında kalan bloğun thread-safe olmasını garanti altına almaktadır. Yeri gelmişken bunu da belirtmeden geçmeyelim, kilitli (locked) durumundaki bir mutexi tekrar kilitlemeye (lock) çalışmak yine belirsizlik durumuna (undefined behaviour) sebep olur.
Dalgınlıktan dolayı unlock() fonksiyonunu çağırmayı unutursa programcı işte o zaman program bloklanır. Bu tarz hatalardan korunmak için yine akıllı göstericilerde kullanıldığı gibi RAII semantiği yardımımıza koşuyor.

lock_guard :

lock_guard ctor’ına geçilen mutex nesnesi, o aşamada kilitleniyor. Daha sonra lock_guard türündeki değişken dtor edildiginde sahip olunan mutex nesnesi ‘unlock’ ediliyor. Yani geliştirici unutsa bile iş otomatik olarak RAII makenizmasına emanet edildiği için başını yastığa rahat koyabilecek. 🙂

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>

std::vector<int> ivec;
std::mutex mtx;
 
void addToVector(int i)
{
    std::lock_guard<std::mutex> grd(mtx);
    ivec.push_back(i);
}

int main()
{
    std::vector<std::thread> tvec;

    for (int i = 0; i < 10; i++) {
        tvec.push_back(std::thread(addToVector, i));
    }
    
    std::for_each(tvec.begin(), tvec.end(), [](std::thread &th) {th.join();});

    
    for (auto i : ivec) 
        std::cout << i << std::endl;
    
    return 0;
}

Yukarıdaki kod örneğinde görüldüğü üzere yaptığım tek değişiklik std::mutex sınıfının lock() unlock() fonksiyonları yerine işi direkt lock_guard şablon sınıfına havale ettim.

unique_lock :

unique_lock şablon sınıfı aslında lock_guard sınıfının daha esnek ve seçenek sunan halidir. Örneğin,

std::unique_lock<std::mutex> ulock (mtx, std::defer_lock);

unique_lock sınıfının ctor ının ikinci parametresine std::defer_lock parametresi geçilerek inşaa aşamasında (construction) lock işleminin yapılmamasını sağlayabilirsin. Daha sonra bu işlemi sağlamak için unique_lock sınıfı lock() ve unlock() fonksiyonlarını hizmet olarak sunmaktadır. Ayrıca unique_lock sınıfı nesneleri kopyalanamazlar (non-copyable) ama taşınabilirlerdir (movable).

#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
#include <algorithm>

std::vector<int> ivec;
std::mutex mtx;
 
void addToVector(int i)
{
    std::unique_lock<std::mutex> grd(mtx, std::defer_lock);
    grd.lock();
    ivec.push_back(i);
    grd.unlock(); //1
}


int main()
{
    std::vector<std::thread> tvec;

    for (int i = 0; i < 10; i++) {
        tvec.push_back(std::thread(addToVector, i));
    }
    
    std::for_each(tvec.begin(), tvec.end(), [](std::thread &th) {th.join();});

    
    for (auto i : ivec) 
        std::cout << i << std::endl;
    
    return 0;
}

1 nolu satırda mutexi unlock etmeyi unutsanız bile bunu sizin yerinize unique_lock sınıfının dtor ı yapacaktır. Bu yüzden direkt mutex sınıfının lock ve unlock fonksiyonlarını kullanmak yerine ihtiyacınıza göre kesinlikle lock_guard ve unique_lock gibi yardımcı sınıfların kullanımlarını alışkanlık haline getirmekte fayda var.

Genel olarak yeni gelen standartlarda mutexler bu şekilde ele alınmış. Burda ekstra deadlock nedir, nasıl korunulur gibi konulara girmedim. Bu konular üzerine ayrıca uzun uzun yazılar yazılabilir. Ama multi-thread programlama yapma niyeti olan arkadaşların kesinlikle deadlock gibi durumlardan korunacak disiplinleri kazanması ve bu konuda okumalar yapması şiddetle tavsiye edilir.

C++11 – std::thread

C++ programlama dilinin en büyük eksikliklerinden biri de built-in concurrency desteğiydi. Aslında bu sorunu uzun zamandır C programcıları posix thread kütüphanesi ile çözüyordu. C++ programcıları ise boost kütüphanesinin bir parçası olan thread kütüphanesini kullandılar. C++11 ile artık dile multi-threading desteği boost kütüphanesinin boost::thread kütüphanesinin büyük bir kısmı miras alınarak dilin standardına eklendi.

C++ konferanslarında dinlediğim, boost::thread in yazarlarından Anthony Wiliams’ ın kitabını okuyarak edindiğim bazı bilgileri bu blogta türkçe kaynak olması amacıyla paylaşmayı uygun gördüm. Olabildiğince örneklendirerek anlamayı ve kullanım hatalarını en aza indirmeyi amaçlıyorum.

O zaman ilk olarak geleneksel ‘Hello World’ ü thread ile oluşturalım.

#include <iostream>
#include <thread>



using namespace std;

void threadFunc()
{
    cout << "Hello from Thread" << endl;
}


int main(int argc, char** argv) 
{
    thread th (threadFunc);
    
    
    th.join();

    return 0;
}

std isim alanı altında bulunan thread sınıfı görüldüğü üzere ctor fonksiyonuna thread de çalıştırılması düşünülen fonksiyonun adresi geçilerek inşaa edilir. Daha sonra ise ‘th’ isimli yeni bir thread oluşturan ana thread oluşturduğu threadin işinin bitmesini thread sınıfındaki ‘join’ fonksiyonunu çağırarak sağlar. Bu noktada ana thread bloke olur ve ‘th’ isimli threading işi bitene kadar bekler. Bu konuya ikinci bölümde daha detaylı ele alacağız.

Sadece fonksiyon göstericileri ile degil istenirse C++11 ile gelen özelliklerden biri olan lambda ifadeleri ve Functor (fonksiyon çağırma operatörü overload edilmiş nesne) nesneler ile de ctor çağırılabilir.

#include <iostream>
#include <thread>

using namespace std;

struct SFtor
{
    void operator ()()
    {
        cout << "Hello from Ftor" << endl;
    }
};

int main()
{
    SFtor sf;
    
    
   thread th([]() {
       
       cout << "Hello from Lambda" << endl;
   });
   
   thread th1(sf);
   
   
   th.join();
   th1.join();
   
   return 0;
}

Performans odaklı program yazıyorsanız functor kullanmanız lehinize olur inline olarak değerlendirilmelerinden ötürü daha hızlı işler. Tabi bu mesele standart olarak değerlendirmek mümkün değil, derleyici bağımlıdır.

Threadlere Parametre Geçmek
Thread sınıfı değişken parametreli şablon fonksiyonlarına (variadic templates) güzel bir örnektir. Bu sayede thread değişkenine geçilen fonksiyonunun parametresini thread sınıfının ctor ına 2. parametre olarak geçebilirsiniz.

#include <iostream>
#include <thread>

using namespace std;

struct SFtor
{
    void operator ()(string const &s)
    {
        cout << "Hello from Ftor " << s <<endl;
    }
};

void threadFunc(int a)
{
    cout << "Hello from traditional func " << a << endl;
}

int main()
{
    SFtor sf;
    
    
   thread th(threadFunc, 5);
   
   thread th1(sf, "cpp istanbul");
   
   
   th.join();
   th1.join();
   
   return 0;
}

Örneklerde görüldüğü gibi thread sınıfının ctorının ilk parametresinden sonra girilen argümanlar çağırılacak olan fonksiyon nesnesine parametre olarak geçiliyor. Yalnız burada dikkat edilmesi gereken noktalardan biri parametreler thread fonksiyonuna geçilirken kopyalanarak aktarılır. Eğer referans olarak geçmek istersek std::ref veya std::cref fonksiyonları kullanılır.

#include <iostream>
#include <thread>

using namespace std;


void threadFunc(int &a)
{
    cout << "thread: " << a << endl;
    a++;
}

int main()
{
   int a = 5;
    
   thread th(threadFunc, ref(a));
   
   th.join();
    
   cout << "main thread: " << a << endl;
    
   return 0;
}

Yukarıdaki örnekte de değişken threade referans olarak geçilerek fonksiyon gövdesinde bir arttırlıyor. Kodu çalıştırdığımızda sonuç olarak ekrana:

thread: 5
main thread: 6

yazacaktır.

Thread fonksiyonuna lambda ifadeleri, Functorlar ve normal fonksiyonlar haricinde bir sınıfın üye fonksiyonunu da geçebilirsiniz. Eğer daha önce std::bind fonksiyonunu kullandıysanız benzer bir sentaksa sahip olduğunu hemen farkedeceksiniz.

Örneğin, ilk parametreye üye fonksiyon adresi daha sonra ilgili sınıfın nesnesi daha sonra ise sırayla fonksiyona eğer varsa parametreler geçilir.

#include <iostream>
#include <thread>

using namespace std;

struct SMemFunc
{
    void foo(string &s)
    {
        s+= " istanbul";
    }

};



int main()
{
    SMemFunc mf;
    string s("cpp");
    
    cout << "before join s: " << s << endl;
    
    thread th(&SMemFunc::foo, mf, ref(s));
    
    th.join();
    
    cout << "after join s: " << s << endl;
     
   return 0;
}

Yukarıdaki örnek çalıştığında referans yoluyla SMemFunc sınıfına geçilen ‘s’ nesnesi fonksiyonda sonuna ‘istanbul’ eklenir. Kısacası çalıştırılan program ekrana:

before join s: cpp
after join s: cpp istanbul

yazdırılır.

Devam edecek…

New Meetup: Reasoning on transactional programming

Verifying Programs under Snapshot Isolation and Similar Relaxed Consistency Model

In this talk, I will present a static verification approach for programs running under snapshot isolation (SI) and similar relaxed transactional semantics. Relaxed conflict detection schemes such as snapshot isolation (SI) are used widely. Under SI, transactions are no longer guaranteed to be serializable, and the simplicity of reasoning sequentially within a transaction is lost. In this paper, we present an approach for statically verifying properties of transactional programs operating under SI. Differently from earlier work, we handle transactional programs even when they are designed not to be serializable.

I will present a source-to-source transformation which augments the program with an encoding of the SI semantics. Verifying the resulting program with transformed user annotations and specifications is equivalent to verifying the original transactional program running under SI — a fact we prove formally. Encoding which I present preserves the modularity and scalability of VCC’s verification approach. I will show the application of the method successfully to benchmark programs from the transactional memory literature.

by İsmail Kuru

PS: Time and Place will be determined in related with participant count and suggestions.

Not: Şimdilik yer ve zaman konusu net değil. Katılımcı sayısı ve istekleri doğrultusunda kesin olarak karar verilecektir.

http://goo.gl/GFwDVY

C++11- nullptr != NULL

C++11 standartlarıyla beraber C++ programlama dili yavaş yavaş daha deterministik olmaya başladı. Yani belirsiz durumlar, kullanım hataları dilin standartlarının desteğiyle en aza çekiliyor. Bu kapsamda nullptr de uzun zamandır bilinen bir sorunu çözüyor.

#include <iostream>
#include <memory>


using namespace std;



void func(int x) 
{ 
    cout << "int" << endl; 
}

void func(char *y) 
{
    cout << "pointer" << endl; 
}
 
int main() {
    func(0);        // int overload
    func((char *)0); // char * overload
    func(nullptr);  // char* overload
    func(NULL);     // main.cpp:23:14: error: call of overloaded ‘func(NULL)’ is ambiguous (compiler: g++5)
}

Üstteki hata aslında derleyiciden derleyiciye farklı olabilir, örneğin visual studio da ‘int’ olarak yorumlanabilir. Kısacası non-deterministic bir durum sözkonusu, derleyicinden derleyiciye farkediyor.
Aslında sorun C++ programlama dilinin iki temel özelliğinden kaynaklanıyor; Function Overloading ve Otomatik Tür dönüşümü. Şimdi bir de NULL isminin nasıl tanımlandığına bakalım:

#if defined (_STDDEF_H) || defined (__need_NULL)
#undef NULL		/* in case <stdio.h> has defined it. */
#ifdef __GNUG__
#define NULL __null
#else   /* G++ */
#ifndef __cplusplus
#define NULL ((void *)0)
#else   /* C++ */
#define NULL 0
#endif  /* C++ */
#endif  /* G++ */
#endif	/* NULL not defined and <stddef.h> or need NULL.  */
#undef	__need_NULL

Görüldüğü üzere derleyiciniz cplusplus derleyicisi ise NULL ismi 0 olarak tanımlanmış. İşte tam da burda sıkıntı ortaya çıkıyor.

Derleyici ‘func(NULL)’ dediğinizde hangi fonksiyonu çağıracağını bilmiyor(g++5 için) ya da kullandığınız başka bir derleyici de farklı bir davranış gözlemleyebilirsiniz integer veya char * function overloadlarından biri çağırılabilir. Hülasası başta da belirttiğim gibi C++11 bu tarz non-deterministic konulara da el atıyor ve bu meseleyi nullptr ile çözüyor. Artık NULL yazdığınız her yere nullptr yazarak daha güvenli programlama yapmış olabilirsiniz özellikle cross-platform yazılım geliştiyorsanız.

nullptr’ nin aslında türü std::nullptr_t ve bir prvalue’ dur. Dolayısıyla herhangi bir şekilde deference edilemez.

Bunun yanı sıra nullptr meta programmingte perfect forwardinge de olanak sağlamaktadır. Şöyle ki:

#include <iostream>
#include <memory>


using namespace std;


template<class F, class A>
void foo(F f, A a)
{
    f(a);
}
 

void func(char *y) 
{
    cout << "pointer" << endl; 
}
 
int main() {
    func(0);        // int overload
    func((char *)0); // void * overload
    func(nullptr);  // void * overload
    
    //foo(func, NULL); //error: invalid conversion from ‘long int’ to ‘char*’ [-fpermissive]
    foo(func, nullptr);   
}

foo şablon fonksiyonunun ikinci parametresine NULL verirsek derleyici f fonksiyonunu integer bir değerle çağırmaya çalışacak. Çünkü şablon parametresi olarak NULL ‘ ın türü integer tespit edildi. nullptr ile çağırdığımızda ise türü std::nullptr_t olarak tespit edileceğinden sorunsuzca ‘f’ veya ‘func’ fonksiyonu sorunsuzca çağırılacaktır.

Aslında nullptr nin implementasyonu bir const class’dır. Merak edenler için standart taslağında implementasyonu şu şekildedir:


const // this is a const object...
class {
public:
 template<class T> // convertible to any type
 operator T*() const // of null non-member
 { return 0; } // pointer...
 template<class C, class T> // or any type of null
 operator T C::*() const // member pointer...
 { return 0; }
private:
 void operator&() const; // whose address can't be taken
} nullptr = {};

C++11 – Zeki, Akıllı Göstericiler (Smart Pointers)

C++ programlama dili, atası C dilinden aldığı özelliklerden biri de memory yönetimidir. Ham göstericilerle (raw pointer) rahatlıkla hafızayı yönetebilirsiniz. ‘new’ operatörü ile aldığınız bellek bloğunu kolaylıkla ‘delete’ operatörüyle iade edebilriz. Fakat bu beraberinde riskleri de beraberinde getiriyor.

Memoryye iade edilmeyen yani ‘delete’ edilmeyen nesneler üzerinden bu bellek bloğuna erişilmeye çalışıldığında çalışma zamanı hatalarıyla (run time error) karşılaşıyor, signal 11 (segmentation fault) gibi kernelden uyarılar alabilirsiniz. İşte bu noktada C++’ ın en güçlü yanlarından olan RAII’ ye dayanan dinamik memory tahsisini de nesnelerin ömürlerine bağlayan sınıflar tasarlandı. Kullanım amaçlarına göre farklılık genel olarak değişse de amaç; hatalara neden olan new, delete operatörleriyle açıktan açığa bellek tahsisini kodumuzda yapmamak.

auto_ptr < T > (Deprecated):
C++ 98 standartlarıyla beraber dile katılan şablon sınıfı auto_ptr temel olarak bellek alanlarının tahsisi ve geri iadesi üzerine odaklandı. Fakat yanlış kullanımı nedeniyle memory hatalarından kaçalım diye tasarlanan akıllı göstericilerin amacının tam tersi şekilde memory hatasına neden oldu. Aşağıdaki kodu inceleyelim.

#include <iostream>
#include <memory>

using namespace std;

struct s
{
    int a;
    int c;
    string b;
    
    s(){cout << "S ctor" << endl;}
    ~s(){cout << "~S dtor" << endl;}
};


void func(auto_ptr<s> e)
{
    e->a = 99;
}

int main() 
{    
    auto_ptr<s> aptr(new s);
    
    cout  << "func begin" << endl;    
    func(aptr);
    cout  << "func end" << endl;      
    
    cout << aptr->a << endl;
     
}

Örnek kodda aptr nesnesi ile ‘f’ fonksiyonu çağrıldğında copy ctor çağırılacak ve aynı nesneyi gösteren bir diğer auto_ptr nesnesi oluşturulacak. Ve bu nesnenin(e) ömrü fonksiyon blokları sonlandığında sonlanacağı için gösterdiği dinamik bellek alanını da free edecek. Dolayısıyla aptr nesnesi artık free edilmiş bir bellek alanını göstermektedir kısacası dangling pointer hatasına sebep olur. Program seg fault vererek aşağıdaki şekilde sonlanır:

S ctor
func begin
~S dtor
func end

RUN FINISHED; Segmentation fault; core dumped; real time: 90ms; user: 0ms; system: 0ms

auto_ptr sınıfı C++17 standartlarıyla beraber standart kütüphaneden çıkarılacaktır. Dolayısıyla kullanmayın .)

unique_ptr < T >
auto_ptr nin yaptığı hatayı yapmaz, tek bir sahiplik vardır. Copy ctor, operator = fonksiyonlarını =delete şeklinde yasaklar dolayısıyla kullanıcı herhangi bir yerde herhangi bir şekilde nesneyi kopyalamaya çalıştığında derleme hatası alacaktır.

Yukarıdaki örneği unique_ptr ile yeniden yazarsak derleme zamanında aşağıdaki gibi bir hata alırız:

main.cpp:29:14: error: use of deleted function ‘std::unique_ptr&amp;amp;amp;lt;_Tp, _Dp&amp;amp;amp;gt;::unique_ptr(const std::unique_ptr&amp;amp;amp;lt;_Tp, _Dp&amp;amp;amp;gt;&amp;amp;amp;amp;) [with _Tp = s; _Dp = std::default_delete&amp;amp;amp;lt;s&amp;amp;amp;gt;]’

Yani ‘deleted’ bir fonksiyon olan copy ctorı çağıramazsın. Peki bir unique_ptr yi bir fonksiyona veya bir sınıfa nasıl transfer edeceğiz, call by value yerine call by reference ile, tekrardan üstteki örnek:

#include <iostream>
#include <memory>


using namespace std;


struct s
{
    int a;
    int c;
    string b;
    
    s(){cout << "S ctor" << endl;}
    ~s(){cout << "~S dtor" << endl;}
};


void func(unique_ptr<s> &e)
{
    e->a = 99;
}

int main() 
{    
    unique_ptr<s> uptr(new s);
    
    cout  << "func begin" << endl;    
    func(uptr);
    cout  << "func end" << endl;      
    
    cout << uptr->a << endl;
     
}

Yapıtığım tek şey ‘void func(unique_ptr)’ fonksiyon imzasını ‘void func(unique_ptr &)’ ile değiştirmek böylelikle sahiplik değişmeden unique_ptr sınıfını func fonksiyonuna parametrik olarak geçirmiş oluruz.Ayrıca std::move kullanarak da sahipliğini ‘move’ ederek değiştirebiliriz. En nihayetinde auto_ptr de karşılaştığımız bellek(memory) sorunlarından da kurtuluyoruz.

shared_ptr< T >
Her ne kadar unique_ptr ile bellek hatalarını izole etsekte kullanım olarak hala sıkıntılarımız mevcut. Ham (raw) göstericilerdeki özellikleri olabildiğince gerçek hale getirmek akıllı göstericilerin implementasyonuna yön veren amaçlardan biri. Öyleyse bir den fazla pointerın aynı bellek alanına işaret etmesini de gerçeklemek gerekiyor.

Tam bu nokta da shared_ptr devreye giriyor. shared_ptr aslında bir referans sayacı tutuyor. shared_ptr ctor’ında başlangıç değeri olarak ‘new’ ile oluşturulan nesne verildiğinde aslında iki tane bellek tahsisi mevcut. Birincisi direkt zaten ‘new’ ile tahsis edilen nesne ikincisi ise referans sayacını kontrol eden, yöneten nesne(Manager Object). Örneğin sp1 ilk oluşturulan nesnemiz olsun içinde bir de Manager Object oluşturulacak. Diğer oluşturulan shared_ptr nesneleri için bu nesne de bulunan sayaç bir arttırılacak. Tam tersi şekilde Manager object içindeki shared sayacı ve weak sayacı sıfırlandığında ise heap bellek alanından tahsis edilen nesne free edilecek.

Yine aynı örnek üzerinden gidersek:

#include <iostream>
#include <memory>


using namespace std;


struct s
{
    int a;
    int c;
    string b;
    
    s(){cout << "S ctor" << endl;}
    ~s(){cout << "~S dtor" << endl;}
};


void func(shared_ptr<s> e)
{
    e->a = 99;
    cout << "count of shared pointer:" << e.use_count() << endl;
}

int main() 
{    
    shared_ptr<s> sptr(new s);
    
    cout  << "func begin" << endl;    
    func(sptr);
    cout  << "func end" << endl;      
    
    cout << sptr->a << endl;
     
}

Görüldüğü üzere func fonksiyonunu referansla çağırmama rağmen herhangi bir bellek hatasıyla karşılaşmıyorum. Burda Manager object, shared pointer sayacını bir arttırıyor. Func fonksiyonu sonunda ise yine sayacı bir azaltıyor(Argüman olarak yaratılan ‘shared_ptr’ türünden ‘e’ nesnesinin ömrü sona erdiği için). Sonrasında process sorunsuz olarak sonlanıyor.

make_shared< T >: Yukarıda bahsettiğimizi gibi shared pointer oluşturulduğunca 2 kez dinamik bellek tahsisi yapılır. Birisi yönetici nesne için diğeri ise T türü için. Performans açısından bu ikisini aynı anda tahsis etmek maliyetli bir işlemdir. Bu soruna standart kütüphaneden make_shared< T > ile çözüm bulunmuş. make_shared< T > kullanıldığında aynı bir kez bellek tahsisini hem T türü için hem de yönetici nesne için beraber yapar. Yani tek bir blok heapten alınır bu da tasarımı itibarıyla pahalı işlem olan dinamik bellek tahsisi teke düşmüş olur. Yine yukarıdaki örnekten devam edersek:

//shared_ptr<s> sptr(new s);
shared_ptr<s> sptr = make_shared<s>();

şeklinde değiştirmek yeterli olacaktır.

weak_ptr < T >
Raw pointer aracılığıyla oluşturulamaz shared_ptr veya başka bir weak_ptr nin kopyalanması, atanmasıyla yaratılır. shared_ptr < T > gibi çalışır ama nesnenin ömrüne müdahalede bulunmaz. Yönetici nesnede (Manager Object) sayaç sıfır dahi olsa hiçbir şekilde tahsis edilen objenin ömrüne etki de bulunmaz. Sadece okuma hakkında sahiptir. Ownership (sahiplik) sadece shared_ptr dedir. Tipik olarak nesneye erişiminizin gerektiği fakat sayacı artırmanıza gerek olmadığı durumlarda kullanılır. Ayrıca lock() fonksiyonu ile weak_ptr < T > işaret ettiği shared_ptr elde edilebilir.

#include <iostream>
#include <memory>


using namespace std;


struct s
{
    int a;
    int c;
    string b;
    
    s(){cout << "S ctor" << endl;}
    ~s(){cout << "~S dtor" << endl;}
};


void func(shared_ptr<s> e)
{
    e->a = 99;
    cout << "count of shared pointer:" << e.use_count() << endl;
}

int main() 
{    
    shared_ptr<s> sptr = make_shared<s>();
    {
        //weak_ptr<s> wptr (new s);   error: main.cpp:29:33: error: no matching function for call to ‘std::weak_ptr<s>::weak_ptr(s*)’
        weak_ptr<s> wptr = sptr;
        cout << "count of weak pointer:" << wptr.use_count() << endl;
    }

    
    cout  << "func begin" << endl;    
    func(sptr);
    cout  << "func end" << endl;      
    
    cout << sptr->a << endl;
     
}

enable_shared_from_this < T >: weak_ptr < T > ‘ nin kullanıldığı yerlerden biridir. Boost kütüphanesinin C++ diline kazandırdığı bir diğer özellikdir. Örneğin bir üye fonksiyondan başka bir fonksiyona ‘this’ pointerını geçmek istediğinizde eğer nesne shared_ptr tarafından kontrol ediliyorsa bir diğer shared_ptr objesi yaratmak durumunda kalacaksınız. Bu da yine dangling pointer ‘a sebep olacak. Örneğin:

void printb (struct s *ps)
{
    cout << ps->b << endl;
}


struct s
{
    int a;
    int c;
    string b;
    
    s(){cout << "S ctor" << endl;}
    ~s(){cout << "~S dtor" << endl;}
    printB() 
    {
        printb(this);
        
    }
};

bu kodu shared_ptr ile gerçeklediğimiz düşünelim


void printb (shared_ptr<s> ps)
{
    cout << ps->b << endl;
}


struct s
{
    int a;
    int c;
    string b;
    
    s(){cout << "S ctor" << endl;}
    ~s(){cout << "~S dtor" << endl;}
    printB() 
    {
        shared_ptr<s> t(this); // !hata: tehlike aynı objeye iki Manager Object oluşturuldu.
        printb(s);
        
    }
};

Burda görüldüğü üzere aynı bellek alanını iki adet manager object yönettiğinde herhangi birinin sayacı sıfırlandığında gösterdiği obje free edileceğinden yine bir dangling pointer olma durumu mevcut. Bunu engellemek için sınıf içerisinde bir tane weak_ptr < T > barındırarak nesnenin ömrüne etki etmiyoruz. Bunu da türetme yöntemiyle şu şekilde implemente ediyoruz:

void printb (shared_ptr<s> ps);
struct s : public enable_shared_from_this<s>
{
    int a;
    int c;
    string b;
    
    s(){cout << "S ctor" << endl;}
    ~s(){cout << "~S dtor" << endl;}
    void printB() 
    {
        shared_ptr<s> sptr = shared_from_this(); // burda ikinci bir manager object üretilmiyor, sadece shared_ptr counter 1 arttırılıyor.
        printb(sptr);
        
    }
};

void printb (shared_ptr<s> ps)
{
    cout << ps->b << endl;
}

Her ne kadar smart pointers size daha güvenli bir kullanım sunsa da yine de dikkat edilmesi gereken yerler var. Özellikle unique_ptr, shared_ptr ve weak_ptr şablon sınıflarının kullanımlarının iyi bir şekilde öğrenilmesi ve uygulamayla pratik yapılmasında fayda var. He bir de auto_ptr ‘ yi aklınıza bile getirmeyin 🙂

C++11 – Derleme zamanında tür belirleyiciler (type traits)

Basitçe derleme zamanında tür hakkında bilgi edinmemizi sağlarlar. Kullanımları oldukça basittir:


#include <iostream>
#include <memory>
#include <type_traits>

using namespace std;

class A
{
public:
    ~A(){cout << "~A()" << endl;}
     A& operator=(const A&) { return *this; }
};

int main() 
{    
    cout << boolalpha << endl;
    
    cout << is_object<int>::value << endl;
    cout << is_object<int&>::value << endl;
    cout << is_object<A>::value << endl;
    cout << is_object<A&>::value << endl;
    
    cout << "-----------------------------" << endl;
    
    struct B {};
 
    enum class C {};   
    
    cout << is_class<A>::value << endl;
    cout << is_class<B>::value << endl;
    cout << is_class<C>::value << endl;
    cout << is_class<int>::value << endl;    
    
    cout << "-----------------------------" << endl;
    
    function<void()> f = [](){cout << "is function object move assignable ?" << endl;};

    cout << is_move_assignable<B>::value << endl;
    cout << is_move_assignable<C>::value << endl;
    cout << is_move_assignable<decltype(f)>::value << endl;
    cout << is_move_assignable<A>::value << endl;
 
}

Başka bir kullanım alanları ise şablon (template) fonksiyonları için bir nevi koşul veya kısıtlama getirmek için de kullanılırlar. Tipik olarak std::enable_if ile kullanılırlar, std::enable_if temel olarak SFINAE (Substitution failure is not an error) prensibine dayanır. Kısacası derleyici, şablon fonksiyon overloadlarını yazarken oluşan hatayı görmezden gelir ve diğer fonksiyonları yazmaya devam eder., Örneğin:

using namespace std;

class A
{   
public:
    
    friend ostream &operator<<(ostream &os, const A&) 
    {
        os << "class A" << endl; 
    }
};

template <class T>
typename enable_if<is_integral<T>::value, T>::type
println(T a)
{
    cout << "if T is a integral type: " << a << endl;
};


template <class T>
typename enable_if<is_object<T>::value, T>::type
println(T &a)
{
    cout << "if T is a object type: " << a << endl;
};

int main(int argc, char**argv) 
{
    
    A a;
    
    println(5);
    println(a);
    
    return 0;
}

Örnekte görüldüğü üzere println fonksiyonu iki farklı şekilde şablonu üretiliyor. “enable_if::value, T>::type” ifadesi ile sadece istenen türlerle o fonksiyonun çağrılmasına olanak veriyor. is_object aslında derleme zamanında gelen türün nesne mi olup olmadığını test etmemizi sağlıyor. Böylelikle kullanıma göre derleme zamanında uygun şablon fonksiyonlar yazılacak.

type traitslerin aşağıdaki linkte bir listesini bulabilirsiniz:

http://en.cppreference.com/w/cpp/types

C++11 – std::tuple üzerine

STL’ i kullananlar bilirler genelde iki farklı türden anlamsal birliktelikleri olan değişkenleri tutmak için genelde std::pair kullanılırdı.

pair<int, string> pt (1, "herbibk");
cout << pt.first << "-" << pt.second << endl;

C++11 ile gelen veri yapılarından biri olan std::tuple’ la birlikte bu özellik sınırsız türde nesneyi sınırsız sayıda içermeye kadar gitti. Üstelik derleme zamanında veri yapısındaki nesne sayısını da biliyorsunuz.

Aslında tuple’ ın nasıl implemente edildiğine baktığınızda basitçe C++’ ın iki temel özelliğini kullanılmış; Çoklu Türetme (Multiple Inheritance) ve Değişken parametreli şablonlar (variadic templates).

template <typename T>
struct tuple_leaf
{
T value_;
};

template <typename... Types>
class tuple : tuple_leaf<Types>...
{
};

‘tuple_leaf…’ parametre pack’i (türkçesini daha düşünmedim) ifadesi tuple_leaf, tuple_leaf, tuple_leaf….tuple_leaf şeklinde açılacak böylelikle tuple sınıfı da bunlardan çoklu türetmeyle türetilecek.

Fakat burda şöyle bir sıkıntı var, aynı türde nesneler geldiğinde örneğin T1 ve T3 string olduğunda bu yapı çalışmayacak zaten var olan bir sınıf için ikinci defa bir sınıf yazılmayacak ve derleme hatası verilecektir. STL de bu sıkıntıyı aşmak için std::integer_sequence kullnılmış derleme zamanında indis üretmek için. Böylelikle aynı tür nesneler geldiğin de bile indisleri farklı olduğundan farklı bir sınıftan türetiliyormuş gibi olacak. Biraz zorlama bir trick olmuş .)

Peki daha önceki variadic template fonksiyonlarından bahsettiğimiz yazılarımdaki1 gibi recursive bir tanım yaparsak bu sorunları aşabilir miyiz ?

template <class... Ts> struct tuple {};

template <class T, class... Ts>
struct tuple<T, Ts...> : tuple<Ts...> {
tuple(T t, Ts... ts) : tuple<Ts...>(ts...), tail(t) {}

T tail;
};

Yukarılda görüldüğü gibi rekürsiv olarak her bir tür için bir sınıf diğer kalan türlerden türetilerek oluşturulacak.

Aslında şöyle bir yapı ortaya çıkacak;

struct tuple<T1, T2, T3> : tuple<T2, T3> {
T1 tail;
}

struct tuple<T2, T3> : tuple<T3> {
T2 tail;
}

struct tuple<T3&>: tuple {
T3 tail;
}

struct tuple {
}

Rekürsiv olarak tanımlamak biraz kafa karıştırsa da en kısa yol. Standart kütüphaneye bakıldığında bu işi non-recursive yoldan halletmişler. Tuple implementasyonunu meta programming bakış açınızı güçlendirmek adına inceleyebilirsiniz.

Göz at:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf
http://blogs.microsoft.co.il/sasha/2015/01/12/implementing-tuple-part-1/

C++11 – Üye fonksiyonlarda ‘default’ ve ‘delete’ manipülasyonu

C++03 standartlarına göre boş bir sınıf tanımlasanız bile derleyici tarafından bir tane default constructor, copy ctor(constructor) ve atama operatörünü (operator=) sizin için oluşturuyordu.

C++11 standartlarıyla beraber direkt olarak derleyicinin bu tarz davranşlarını manipüle etme şansına hakim oluyorsunuz. C++ programlama dilini işte en çok bu yüzden seviyorum, yazılımcıya olabildiğince fazla hareket alanı sağlıyor.

Tipik bir örnek olarak eğer sınıfınıza herhangi bir ctor tanımladığınızda derleyici sizin için ctor tanımlamayacaktır. Aşağıdaki kodu inceleyelim;


#include <iostream>

using namespace std;

class herbibk
{
public:
    herbibk(int a) {};
};

int main(int argc, char**argv)
{

    herbibk hbk(5); //ok
    herbibk hb; //hata derleyici default ctor oluşturmadığı için:
                      //main.cpp:23:13: error: no matching function for call to ‘herbibk::herbibk()’

    return 0;
}

C++11 ile gelen yeni özellikle beraber derleyiciye kardeşim sen yine bana default ctor üret diyebiliyoruz;

#include <iostream>

 
using namespace std;
 
class herbibk
{
public:
    herbibk(int a) {};
    herbibk()  = default;
};
 
int main(int argc, char**argv) 
{
 
    herbibk hbk(5); //ok    
    herbibk hb; //ok
    

 
    return 0;
}

Görüldüğü üzere artık derlerken hata almıyoruz.

herbibk()  = default;

İfadesini sadece sınıfın tanımına ekleyerek derleyici tarafından oluşturulan default ctor’un her zaman oluşturulmasını sağladık.

Başka bir kullanımı ise, daha önce C++03 zamanıda : ) bir sınıfı kopyalanamaz yapmak için copy ctor’unu ve atama operatörünü private yapılırdı. Şimdi bizim ‘delete’ imiz var hatta operator new’ i de ‘delete’ edebiliriz, Şöyle ki;

#include <iostream>

 
using namespace std;
 
class herbibk
{
public:
    herbibk(int a) {};
    herbibk()  = default;
    herbibk(const herbibk&) = delete;
    herbibk& operator=(const herbibk&) = delete; 
    void* operator new(std::size_t) = delete;
    void* operator new[](std::size_t) = delete; 
};
 
int main(int argc, char**argv) 
{
 
    herbibk hbk(5); //ok    
    herbibk hb(hbk); //hata  derleyiciye copy ctor fonksiyonunu oluşturma demiştik : main.cpp:21:19: error: use of deleted function ‘herbibk::herbibk(const herbibk&)’
    
    hb = hbk; // hata derleyiciye atama operatörü fonksiyonunu oluşturma demiştik 🙂  : main.cpp:23:8: error: use of deleted function ‘herbibk& herbibk::operator=(const herbibk&)’
    
    herbibk *pHbk = new herbibk[4]; // hata main.cpp:25:34: error: use of deleted function ‘static void* herbibk::operator new [](std::size_t)’
    herbibk *pHbk = new herbibk; // hata main.cpp:26:25: error: use of deleted function ‘static void* herbibk::operator new(std::size_t)’

    return 0;
}

‘delete’ anahtar kelimesi otomatik tür dönüşümünü engellemek amacıyla tüm üye fonksiyonlara uygulanabilir, Örneğin;

#include <iostream>

 
using namespace std;
 
class herbibk
{
public:
    herbibk(int a) {};
    herbibk()  = default;
    herbibk(const herbibk&) = delete;
    herbibk& operator=(const herbibk&) = delete;
    void f(int a) {};
    void f(double &) = delete;
};
 
int main(int argc, char**argv) 
{
 
    herbibk hbk(5); //ok    
    herbibk hb;
    
    hb.f(5); // ok sorun yok
    hb.f(5.); // ok sorun yok, çünkü otomatik olarak 5.0 int türüne dönüştürüldü.
    
    return 0;
}

Ayrıca bu kullanımı fonksiyon şablonlarını (function template) kullanarak genelleştirebiliriz; Örneğin f fonksiyonunu integer türü haricindeki hiçbir argümanla çağıramayalım:

#include <iostream>

 
using namespace std;
 
class herbibk
{
public:
    herbibk(int a) {};
    herbibk()  = default;
    herbibk(const herbibk&) = delete;
    herbibk& operator=(const herbibk&) = delete;
    void f(int a) {};
    template<typename T> void f(T) = delete;
};
 
int main(int argc, char**argv) 
{
 
    herbibk hbk(5);  
    herbibk hb;
    
    hb.f(5); // ok sorun yok 
    hb.f(5.00); // hata: main.cpp:26:14: error: use of deleted function ‘void herbibk::f(T) [with T = double]’
    
    return 0;
}