Evet arkadaşlar uzun bir süredir radarımda olan fakat bir türlü yazıya dökemediğim bir konu olan değişken şablonlar (“variadic templates”) konusuna bakıyor olacağız. Kabiliyet her ne kadar C++ 11 ile sunulmuş olsa da sonraki C++ standartlarında da, bir takım güncellemelere ve ilavelere tabi olmuş. Bu kabiliyeti aslında “template metaprogramming” ile uğraşan arkadaşlar muhtemelen oldukça sık kullanıyorlardır, açıkçası ben de her ne kadar ne işe yaradığını bilsem de, çok fazla bu kabiliyeti kullanmadım. O sebeple burada çok detaylı bir inceleme bekliyorsanız, sizi baştan uyarayım. Fakat bunu, daha ileri seviye kullanımları için ilk basamak olarak görebilirsiniz 😉
İçerik
Şablonlara (“templates”) Kısa Bir Bakış
Asıl konuya dalmadan önce, hızlıca “template” lar nasıl çalışıyora bakmakta fayda var diye düşünüyorum. Bu bağlamda da, hemen bir örnek ile olaya girelim. Örneğin, elimizde tam sayıları toplamak için kullandığımı oldukça sofistike bir fonksiyon olduğunu düşünelim:
1 2 3 4 |
int32_t Topla(int32_t argLeft, int32_t argRight) { return argLeft + argRight; } |
Şimdi bunun benzerine float sayılar için de ihtiyaç duyduğumuzu düşünelim ne yapacağız? Ya benzer bir fonksiyonu float için de tanımlayacağız ya da “template” ları kullanacağız. Nasıl mı? Hemen bakalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> template <typename T> T Topla(T argLeft, T argRight) { return argLeft + argRight; } int main(){ std::cout << "3 + 5 = " << Topla(3, 5) << '\n'; std::cout << "3.3F + 5.3F = " << Topla(3.3, 5.5) << '\n'; return 0; } |
Evet gördüğünüz gibi tek bir tanımlama ile birden fazla tip için oldukça sofistike bir toplama kabiliyeti elde etmiş olduk. Peki arka planda ne oluyor? Bunu hiç merak ettiniz mi? Hemen bakalım. Bunun için de, uzun bir süredir kullandığım https://gcc.godbolt.org/ sitesini kullanacağız. Bu site aracılığı ile yazmış olduğunuz koda ilişkin üretilen makine kodunu (“assembly”) farklı derleyiciler için görebiliyorsunuz, daha da güzeli, sunulan renklendirme ile satırlar arası ilişki kurmanıza da olanak sağlıyor. Hemen ilk toplama işlemi için x86/64 Clang 11 çıktısına bakalım (ilgili olmayan kısımları çıkardım fakat merak eden okuyucularım, ilgili sitede tam haline bakabilir):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Orjinal kodumuz int32_t Topla(int32_t argLeft, int32_t argRight) { return argLeft + argRight; } // Makine kodu karşılığı Topla(int, int): # @Topla(int, int) push rbp mov rbp, rsp mov dword ptr [rbp - 4], edi mov dword ptr [rbp - 8], esi mov eax, dword ptr [rbp - 4] add eax, dword ptr [rbp - 8] pop rbp ret |
Şimdi aynı kodun “template” lar kullanılarak yazılan haline bakalım
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// Kodumuz template <typename T> T Topla(T argLeft, T argRight) { return argLeft + argRight; } int main() { Topla(3, 5); return 0; } // Makine kodu karşılığı int Topla<int>(int, int): # @int Topla<int>(int, int) push rbp mov rbp, rsp mov dword ptr [rbp - 4], edi mov dword ptr [rbp - 8], esi mov eax, dword ptr [rbp - 4] add eax, dword ptr [rbp - 8] pop rbp ret |
Burada main içerisinde örnek bir kullanım ekledim, çünkü aksi halde template’a karşılık gelen kod üretilmeyecekti. Peki, iki kod arasındaki farkı görebiliyor musunuz? Göremiyor olmanız lazım çünkü ikisi de aslında aynı kodu üretiyor. Gördüğünüz üzere aslında arka planda, derleyici sizin için yine benzer bir kod üretti. Şimdi kayan sayıları kullanan halini de ekleyelim main içerisine bakalım şimdi ne üretecek:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// Kodumuz template <typename T> T Topla(T argLeft, T argRight) { return argLeft + argRight; } int main() { Topla(3, 5); Topla(3.0F, 5.0F); return 0; } // Uretilen kod int Topla<int>(int, int): # @int Topla<int>(int, int) push rbp mov rbp, rsp mov dword ptr [rbp - 4], edi mov dword ptr [rbp - 8], esi mov eax, dword ptr [rbp - 4] add eax, dword ptr [rbp - 8] pop rbp ret float Topla<float>(float, float): # @float Topla<float>(float, float) push rbp mov rbp, rsp movss dword ptr [rbp - 4], xmm0 movss dword ptr [rbp - 8], xmm1 movss xmm0, dword ptr [rbp - 4] # xmm0 = mem[0],zero,zero,zero addss xmm0, dword ptr [rbp - 8] pop rbp ret |
Umarım yeni eklenen satırlarından neden kaynaklandığını anlamışsınızdır. Kayan sayı için de derleyici, arka planda yeni bir fonksiyon bizler için tanımlamış oldu. Burada, aklınıza derleyici ne zamana kadar bunları üretecek sorusu gelebilir. Kodunuz içerisindeki kullanım miktarı kadar. Eee, bu kodu büyütmez mi? Büyütür. Peki bize sıkıntı yaratır mı? Yaratabilir. Keza, emniyet kritik yazılımlar için takip edilen kodlama standartlarında, bütün olası kullanımların başta tanımlanması zorunlu kılınır. Bizim durumumuzda, kodunuzun bir yerinde aşağıdaki satırları tanımlanmış olmanız gerekiyor, bu da derleyicinin ilgili kodları üretmesini tetikleyecektir (hemen deneyebilirsiniz 😉 :
1 2 |
template float Topla<float>(float, float); template int32_t Topla<int32_t>(int32_t, int32_t); |
Bu noktada, aslında “template” mekanizması ile uygun fonksiyonların derleme zamanında nasıl tanımlandığını ve kullanımının nasıl olduğunu sanırım artık görmüş olduk. Daha detaylı bilgiler için herhangi bir C++ kaynağına başvurabilirsiniz.
Şimdi gelelim asıl konumuza.
Değişken Şablonlar (“Variadic Templates”)
Uzun lafın kısası, değişken şablonlar bize, sıfır ya da herhangi bir sayıda şablon argümanı alan sınıf ya da fonksiyon şablonları tanımlamamıza olanak sağlarlar. Peki daha önce bu yapılamıyor muydu? Güzel soru, evet yapılabiliyordu, fakat ilgili durumda en fazla adet kadar argüman yine de tanımlanması gerekiyordu. Örneğin, bir tuple şablon sınıfını bu şekilde tanımlayacak olsak nasıl yapardık? İşte söyle:
1 2 3 4 5 |
struct Unused; template<typename T1 = Unused, typename T2 = Unused, template<typename T3 = Unused, typename T4 = Unused, template</∗ up to ∗/ typename TN = Unused> class Tuple; |
Bu tanımlama ile N adete kadar argüman alan Tuple sınıfı tanımlayabilirsiniz. Ör:
1 |
typedef Tuple<char, short, int, long, long long> tupleIntegralSet; |
Ya da ilgili adete göre aşağıdaki gibi kullanımlar da mevcut:
1 2 3 4 5 6 7 8 |
template<> class Tuple<> { /∗ Hiç argüman olmaması durumu. ∗/ }; template<typename T1> class Tuple<T1> { /∗ Tek argüman olması durumu. ∗/ }; template<typename T1, typename T2> class Tuple<T1, T2> { /∗ İki argüman olması durumu. ∗/ }; |
Yukarıdaki örneklerden de görebileceğiniz üzere, bu tarz bir kabiliyet için hem bir çok kod yazmak gerekiyor, ilaveten burada yapılacak hatalara ilişkin hata mesajları da pek insancıl olmayabilir 🙂 Bir de elbette bu argüman sayısına ilişkin bir limit de olacak.
İşte değişken şablonlar bize, yukarıdaki Tuple örneğini çok daha kolay ve anlaşılabilir şekilde aşağıdaki gibi tanımlamamıza izin veriyor:
1 2 |
template<typename...Args> class Tuple; |
Bu tanımlama aslında yukarıda yaptığımız tanımlama ile aynı şeyi elde edebiliyoruz.
Şimdi gelelim yukarıdaki tanımlamalara ilişkin hususlara.
Parametre Paketi (“Parameter Pack”)
Öncelikle, Args ibaresinin hemen solundaki “…” ya bakalım (“ellipsis”). Buna şablon tip parametre paketi deniliyor (“template type parameter pack“), Args ise paket ismi olarak ifade edilmektedir. Parametre paketi (“Parameter Pack”), C++ 11 değişken şablonlar ile sunulan bir yapıdır. Normalde tek bir argümana karşılık gelen parametrelerin aksini, bu yapı ile birden fazla argüman tek bir parametre paketi haline getirilebilmektedir.
Örneğin, yukarıda verdiğimiz “typedef tuple<char, short, int, long, long long> integral types;” ile paketlenen parametreler nelerdir? Şunlar: char, short, int, long, long long.
Bu arada, bu parametre paketi içerisine sadece şablon tipleri değil, tip olmayan parametreler ve şablon/şablon parametrelerin kullanımları da mümkün. Hemen örneklere bakalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// Tip şablon parametre paketi kullanımına örnek template<class...Args> class X{}; X<> a; // Boş parametre listesi X<int> b; // Tek parametreli liste X<int, char, float> c; // Üç parametreli liste // Tip olmayan şablon parametre paketi kullanımına örnek template<bool...Args> class X{}; X<> a; // Boş parametre listesi X<true> b; // Tek parametreli liste X<true, false, true> c; // Üç parametreli liste // Şablon şablon parametre paket kullanımına örnek template <template <class T>... class U> // U şablon şablon parametresine örnektir. Daha detaylı örnekler şimdilik beni aşıyor :) ama kaynaklarda bunlara ilişkin örnekler mevcut. |
Peki fonksiyon şablonlarında, parametre paketi kullanımı nasıl oluyor. Ona da bakalım:
1 2 3 4 5 6 |
template<class...A> void exampleFunc(A...Args) exampleFunc(); // void exampleFunc(); exampleFunc(1); // void exampleFunc(int); exampleFunc(1,2,3,4,5); // void exampleFunc(int,int,int,int,int); exampleFunc(1,'x', aWidget); // void exampleFunc(int,char,widget); |
Yukarıda bahsettiğimiz kullanımlara benzer şekilde, 0 ya da daha fazla argüman ile ilgili fonksiyon çağrılabilmektedir. Ayrıca, tek şablon parametreleri ile yukarıda bahsettiğim parametre paketleri de birlikte kullanılabilmektedir. Burada şöyle bir kısıt var, sadece bir parametre paketi olarak ve bu da en sonda tanımlanmalıdır. Ör:
1 |
template <class X, bool B, class... Args> |
Burada bahsetmek istediğim bir konu daha var. Özellikle, bu parametre paketlerini isimlendirirken, çoğul bir isim ya da birden fazla parametreyi ifade eden bir kullanım olmasında fayda olabilir.
Paket Genişletme (“Pack Expansion”)
Buraya kadar parametre paketlerinin nasıl tanımlandığını gördük, güzel, peki bunu nasıl kullanacağız? Daha önce de ifade ettiğimiz üzere, bu parametre paketi aslında, virgül ile ayrılan ve tek tek tanımlamaları içeren bir liste haline gelmektedir. Güzel de bunu, örneğin, fonksiyonlara nasıl geçireceğiz? Bunun için de, paket ismi + “…” kullanımına başvuruyoruz. Evet, zihninizde tam netleşmemiş olabilir, o sebeple hemen bir örnek ile bu kullanıma bakalım:
1 2 3 4 5 6 7 8 9 10 11 12 |
template <class<strong>... Args</strong>> void func(int i, <strong>Args...</strong> args) { // Önce parametre paket ismini koyalım, sonra da ... ekleyelim std::tuple<Args...> argsTuple{ args... }; } // İlk kullanım func(15, 3.14F, "stringExample", 100); // İkinci kullanım func(15); |
Burada, func(15, 3.14F, “stringExample”, 100) çağrısı ile ilgili parametre tiplerine ilişkin std::tuple’ı açık bir şekilde tanımlamadan, ilgili parametre paketini genişleterek tanımlamasını sağlıyoruz. Yani aslında ne tanımladık? Tam olarak aşağıdakini (dikkat ilk parametre olan int, pakete dahil değil):
1 2 3 4 5 |
void func(int i, float args_1, char const* args_2, int args_3) { std::tuple<float, char const*, int> argsTuple{args_1, args_2, args_3}; //... } |
İkinci kullanım, yukarıda bahsettiğim aslında sıfır parametre kullanım durumunu tariflemektedir. Peki bu durumda nasıl bir kod üretiliyor?
1 2 3 4 5 6 |
template<> void func(int i /* sonrası yok */) { // Boş bir tuple :) std::tuple<> argsTuple{}; } |
Tabi, bunu yapabilmek için bir şeye daha ihtiyacımız var. Nedir peki o? tuple’ın da bu şekilde tanımlanmış olması. Hemen bakalım nasıl tanımlanmış:
1 2 |
template< class... Types > class tuple; |
İşte bu sayede, bu kullanım mümkün olmaktadır.
Özet olarak bu kullanımları aşağıdaki örnek ile de özetleyebiliriz:
1 2 3 4 5 |
template<class...Args> void func1(Args...a){}; template<class...Args> void func2(Args...b){ func1(b...); } |
Peki bunlar dışında bu paket genişletme nerelerde kullanılabilir. Aşağıda bunları sıralamaya çalıştım:
- Fonksiyon argüman listeleri (argüman olarak geçirilmesi),
- Şablon argüman listeleri (şablon argümanı olarak geçirilmesi),
- Parantez ilklendirmeleri “()”,
- Küme ayraçlı ilklendirmeler “{}”
- Fonksiyon şablon parametre listeleri,
- Şablon parametre listeleri,
- Üst veya üye ilklendirme listeleri,
- Lambda ifadeleri,
- İstisna (“exception”) tanımlamaları,
- sizeof… operatörü
Her birine ilişkin detaylı açıklama ve örneklere ise https://en.cppreference.com/w/cpp/language/parameter_pack adresinden ulaşabilirsiniz. Yukarıdakilerden özellikle “sizeof…” a değinmek istiyorum bu konuyu kapatmadan.
sizeof…‘da diğer paket genişletme kullanımlarından birisidir. Basitçe, ilgili paketin kaç elemandan oluştuğunu döner. Burada çalışma zamanında boyut bilgisini dönen sizeof aksine, sizeof…, derleme zamanında oluşturulan bir sabit döner. Hemen bir örneğe bakalım:
1 2 3 4 5 6 7 8 9 |
template <typename... Args> void func(Args... args) { std::cout << "Dizi boyutu: " << sizeof...(args) << '\n'; // Geçilen argüman adeti kadar uzunlukta bir dizi oluşturalım int argsTable[sizeof...(args)] = {args...}; } |
Arkadaşlar, sanırım değişken şablonların ne olduğu ve nasıl kullanılabileceğine ilişkin sizlere az da olsa bir fikir verebildiysem ne mutlu. Elbette konu oldukça derin, ayrıca C++ 17 ile de bir takım eklemeler yapılmış (“fold expressions”), buna da inşallah sonraki yazılarımdan birisinde değineceğim. Bu arada bu konuya ilişkin ek bilgi edinmek isteyen yazılımperver dostlarımı kaynaklarda verdiğimi video ve diğer kaynakları incelemeye davet ediyorum.
Bu kabiliyet ile birlikte bir C++ 11 özelliğinin üzerine de çizık atıyoruz. Bir sonraki yazımda görüşmek dileğiyle, bol kodlu günler 🙂
Kaynaklar
- https://en.cppreference.com/w/cpp/language/parameter_pack
- https://www.ibm.com/docs/en/i/7.3?topic=c11-pack-expansion
- https://en.cppreference.com/w/cpp/language/sizeof…
- https://kubasejdak.com/techniques-of-variadic-templates
- http://lbrandy.com/blog/2013/03/variadic_templates/
- https://www.youtube.com/watch?v=o1EvPhz6UNE
- https://www.youtube.com/watch?v=R1G3P5SRXCw
- https://arne-mertz.de/2016/11/modern-c-features-variadic-templates/