Ders 03 / 12
Değişken Sayıda Şablon Parametresi
Parametre paketi, sizeof..., paket açma, özyineli açma ile katlama ifadesi karşılaştırması, std::forward ile iletme ve açma yönteminin derleyiciye kaç gövde yazdırdığı.
Yayın Güncelleme
Bu derste
- Parametre paketini tanımlar ve sizeof... ile paket açmayı kullanır.
- Özyineli açma ile katlama ifadesinin derleyiciye yazdırdığı gövde sayısını karşılaştırır.
- Aynı tip listesinin gövdeyi yeniden kullandığını, yeni bir tipin ise yeni gövdeler doğurduğunu gösterir.
İçindekiler
Önceki ders özelleştirmeyi ölçtü: Paket<bool> bedeli yuvadan çıkarıp koşumdaki bir adıma
koydu. Bu ders şablonun parametre sayısını da serbest bırakıyor. Bir işlev, kaç argüman
verilirse versin, hepsini kabul edebilir mi? Değişken sayılı şablon bunu sağlar, ama
açma yöntemi derleyiciye kaç gövde yazdırdığını değiştirir. Dersin sorusu o sayıdır: bir
paketi açmak kaç gövde doğurur?
Parametre Paketi ve sizeof…
Üç noktalı typename... T bildirimi bir parametre paketi tanımlar: sıfır ya da daha
çok tipi tek bir ad altında toplar. İşlev parametresi T... x ise buna karşılık gelen
işlev parametre paketidir. Paketteki eleman sayısı çalışma zamanında değil derlemede
bilinir ve sizeof...(T) ile okunur. Paketi kullanmak için x... yazılır; buna paket
açma denir: derleyici paketin her elemanını virgülle ayrılmış bir listeye çevirir.
Paketi başka bir işleve iletmek için std::forward kullanılır. Bir argümanın sol değer ya
da sağ değer olduğu bilgisi, A&& biçimindeki parametrede korunur; std::forward<A> o
bilgiyi bozmadan iletir. Aşağıdaki program üç şeyi birlikte gösteriyor.
// paket-islemleri.cpp — sizeof..., iletme ve tekli katlama
#include <cstdio>
#include <utility>
template <typename... T>
constexpr int say(T...) { return static_cast<int>(sizeof...(T)); }
template <typename... T>
bool hepsi_pozitif(T... x) { return (... && (x > 0)); }
static long topla3(long a, long b, long c) { return a + b + c; }
template <typename F, typename... A>
long ilet(F f, A&&... a) { return f(std::forward<A>(a)...); }
int main() {
std::printf("say %d %d %d\n", say(), say(1), say(1, 'a', 2.0));
std::printf("hepsi %d %d %d\n", hepsi_pozitif(1, 2, 3) ? 1 : 0,
hepsi_pozitif(1, -2) ? 1 : 0, hepsi_pozitif() ? 1 : 0);
std::printf("ilet %ld\n", ilet(topla3, 1, 2L, 3));
return 0;
}
say 0 1 3 hepsi 1 0 1 ilet 6
MP13. say boş paketle sıfır, üç argümanla üç verir; sayı derlemede sabittir. hepsi_pozitif
bir katlama ifadesi kullanır: (... && (x > 0)), paketi && işleciyle tek bir
ifadeye katlar. Paket boşken && katlaması true verir. ilet argümanları hiçbir
kopya eklemeden topla3e gönderir; std::forward yalnız burada, iletme için gerekli.
Özyineli Açma ve Taban Durumu
Paketi elle açmanın klasik yolu üç adımdır: ilk argümanı al, geri kalanıyla aynı işlevi çağır, paket boşalınca duran bir taban durumu yaz. Her çağrı bir eleman daha eksik bir paketle yapılır, yani her adım ayrı bir şablon gövdesidir.
Toplamı iki yolla kuralım. topla_oz özyineli açar; topla_kat aynı toplamı tek katlama
ifadesiyle verir. Her gövde kendi yerel static’ini ilk çağrıda bir kez artırır, yani
sayaç üretilen gövdeleri sayar.
// toplam.hpp — ozyineli ve katlamali toplam, govdeleri sayar
#ifndef TOPLAM_HPP
#define TOPLAM_HPP
inline int oz_govde = 0;
inline int kat_govde = 0;
inline long topla_oz() {
static const bool kayit = (++oz_govde, true);
(void)kayit;
return 0;
}
template <typename Ilk, typename... Kalan>
long topla_oz(Ilk ilk, Kalan... kalan) {
static const bool kayit = (++oz_govde, true);
(void)kayit;
return ilk + topla_oz(kalan...);
}
template <typename... T>
long topla_kat(T... x) {
static const bool kayit = (++kat_govde, true);
(void)kayit;
return (0L + ... + x);
}
#endif
// paket-acma.cpp — ayni toplam, iki acma yontemi
#include <cstdio>
#include "toplam.hpp"
int main() {
long a = topla_oz(1, 2, 3);
long b = topla_kat(1, 2, 3);
std::printf("toplam %ld %ld\n", a, b);
std::printf("ozyineli %d katlama %d\n", oz_govde, kat_govde);
return 0;
}
toplam 6 6 ozyineli 4 katlama 1
MP14. İki yöntem de altı verir, ama gövde sayıları ayrıdır. topla_oz(1, 2, 3) üç
şablon gövdesi (<int,int,int>, <int,int>, <int>) ve bir taban işlevi kullanır: dört.
topla_kat(1, 2, 3) aynı toplamı tek gövdeyle verir. Çekirdekteki ozyineli paket acma
satırının ozyineli=4 katlama=1 sayıları bu ölçümdür. Bedel derleme sınıfına düşer:
koşumda karar adımı kalmaz, derleyici her açma biçimi için ayrı sayıda kod yazar.
Katlama İfadesinin Biçimleri
Katlama ifadesi dört biçimde yazılır. Tekli katlama (... + x) ya da (x + ...)
yalnız paketi taşır; ikili katlama (0L + ... + x) bir başlangıç değeri ekler. Sol
katlama ((x1 + x2) + x3) gibi, sağ katlama (x1 + (x2 + x3)) gibi gruplar. Başlangıç
değeri taşımayan tekli katlama, boş paket için yalnız &&, || ve , işleçlerinde
geçerlidir: (... && ...) boş pakette true, (... || ...) false, virgül void()
verir. + ile tekli katlama boş pakette derlenmez; topla_kat() çağrısı için 0L
başlangıç değeri bu yüzden gerekli. Katlama ifadesi her somutlaştırmada tek gövdeyle
çalışır, çünkü paket açma bir özyineleme değil, derleyicinin ürettiği tek bir ifadedir.
Açma Kalıbı: Üç Noktanın Sağ Tarafı
Üç noktanın anlamı sağında durduğu ifadeye bağlıdır. x... yalnız x’i açar; ama
(x * 2)... kalıbın tamamını her eleman için bir kez yazar. Kalıp içinde birden çok paket
varsa hepsi aynı uzunlukta olmalıdır ve eleman eleman birlikte açılır. Açma yeri de
önemlidir: işlev çağrısı argüman listesinde, küme başlatıcıda, şablon argüman listesinde ya
da bir katlama ifadesinde açılabilir. Aşağıdaki program bunlardan ikisini, küme başlatıcıyı ve
katlama ifadesini gösteriyor.
// kalip.cpp — kalip acma ve ayni tip sorgusu
#include <cstdio>
#include <type_traits>
template <typename... T>
int ikilisi(T... x) {
int d[] = {static_cast<int>(x * 2)..., 0};
int t = 0;
for (int v : d) {
t += v;
}
return t;
}
template <typename Ilk, typename... T>
constexpr bool hepsi_ayni_v = (std::is_same_v<Ilk, T> && ...);
template <typename... T>
void yaz(T... x) {
int sira = 0;
(std::printf("%s%d", sira++ ? "," : "", static_cast<int>(x)), ...);
std::printf("\n");
}
int main() {
std::printf("ikilisi %d\n", ikilisi(1, 2, 3));
std::printf("ayni %d %d\n", hepsi_ayni_v<int, int, int>,
hepsi_ayni_v<int, int, long>);
yaz(7, 8L, 'a');
return 0;
}
ikilisi 12 ayni 1 0 7,8,97
MP15. ikilisi(1, 2, 3) kalıbı üç kez yazar: {2, 4, 6, 0} kümesi toplamda on iki verir.
Sondaki sıfır, boş pakette {} yerine geçerli bir küme sağlar. hepsi_ayni_v bir tip
sorgusudur ve katlama ifadesi && ile tüm eşleşmeleri toplar. yaz virgül işleciyle
katlanır; virgül katlamasında elemanlar soldan sağa değerlendirilir, bu standardın
garantisidir ve çıktıdaki sıra bunu gösterir (97, a karakterinin kodudur).
İleten Yapıcı: Paket ve std::forward
Paketin en sık görülen kullanımı, bir nesneyi kurarken argümanları olduğu gibi yapıcıya
iletmektir. A&&... biçimindeki parametreler iletme referansı paketi oluşturur ve
std::forward<A>(a)... her elemanın sol değer ya da sağ değer olduğunu korur. Bunu
görmek için kopyalanıp taşınmasını saydığımız bir tip yazalım.
// ileten.cpp — ileten yapici, kopya ve tasima sayisi
#include <cstdio>
#include <utility>
struct Iz {
static inline int kopya = 0;
static inline int tasima = 0;
int deger;
explicit Iz(int d) : deger(d) {}
Iz(const Iz& o) : deger(o.deger) { ++kopya; }
Iz(Iz&& o) noexcept : deger(o.deger) { ++tasima; }
};
struct Sarmal {
Iz a;
Iz b;
template <typename A, typename B>
Sarmal(A&& x, B&& y) : a(std::forward<A>(x)), b(std::forward<B>(y)) {}
};
template <typename T, typename... A>
T olustur(A&&... a) {
return T(std::forward<A>(a)...);
}
int main() {
Iz sol(1);
Sarmal s1 = olustur<Sarmal>(sol, Iz(2));
std::printf("kopya=%d tasima=%d\n", Iz::kopya, Iz::tasima);
Sarmal s2 = olustur<Sarmal>(Iz(3), Iz(4));
std::printf("kopya=%d tasima=%d\n", Iz::kopya, Iz::tasima);
std::printf("degerler %d %d %d %d\n", s1.a.deger, s1.b.deger, s2.a.deger, s2.b.deger);
return 0;
}
kopya=1 tasima=1 kopya=1 tasima=3 degerler 1 2 3 4
MP16. İlk kurulumda sol bir sol değerdir, std::forward<Iz&> onu kopyalar; geçici
Iz(2) bir sağ değerdir ve taşınır. İkinci kurulumda iki geçici de taşınır. std::forward
olmasa her argüman adlı bir sol değer olarak yapıcıya gider ve her seferinde kopyalanırdı.
Sayaçlar birikimlidir, ikinci satır ikinci kurulumun iki taşımasını da içerir; bunlar ürün sayısıdır, süre iddiası değildir. Bu iletme zinciri de her argüman tipi
birleşimi için ayrı bir olustur gövdesi yazdırır, yani bedel yine derleme sınıfındadır.
Paket ve Sınıf Şablonu
Paket yalnız işlevlerde değil sınıf şablonlarında da bulunur. std::tuple<int, double, char>
bunun örneğidir. Kendi tip listemizi taşıyan boş bir sınıf yazıp eleman sayısını
sizeof... ile sorabiliriz; sınıfın kendisi bir değer taşımaz, yalnız tipleri adlandırır.
// liste.cpp — tip listesi tasiyan sinif sablonu
#include <cstdio>
#include <type_traits>
template <typename... T>
struct TipListesi {
static constexpr int boy = static_cast<int>(sizeof...(T));
};
template <typename L>
struct Boy;
template <typename... T>
struct Boy<TipListesi<T...>> {
static constexpr int deger = sizeof...(T);
};
static_assert(TipListesi<int, char>::boy == 2);
static_assert(Boy<TipListesi<>>::deger == 0);
int main() {
using L = TipListesi<int, double, char>;
std::printf("boy %d %d\n", L::boy, Boy<L>::deger);
std::printf("bos %d\n", TipListesi<>::boy);
return 0;
}
boy 3 3 bos 0
MP17. TipListesi<> boş bir pakettir ve geçerlidir; sizeof... sıfır verir. Boy
kısmi özelleştirmesi TipListesi<T...> kalıbıyla paketi geri toplar: önceki dersin kısmi
özelleştirmesi, bu kez paketle. Hiçbir koşum maliyeti yoktur, çünkü iki static_assert
ve üyeler derlemede sabitlenir.
Paketi Saklamak: tuple, apply ve index_sequence
Bir paket doğrudan saklanamaz; bir değişkene atanamaz ve bir kapsayıcıya konamaz. Saklamak
için standart kütüphane std::tuple verir: farklı tiplerden elemanları tek bir değerde
toplar. std::apply bir demetin elemanlarını bir işlevin argümanları olarak açar, yani
demet ile paket arasında köprü kurar. Elemanlara sıra numarasıyla erişmek gerekirse
std::index_sequence<I...> derleme zamanında 0, 1, 2, ... dizisini bir paket olarak
üretir. Bu üç araç özyineleme yazmadan paketin elemanlarını konumlarıyla birlikte işler.
// demet.cpp — tuple, apply ve index_sequence
#include <cstdio>
#include <tuple>
#include <type_traits>
#include <utility>
template <typename T, std::size_t... I>
long agirlikli(const T& t, std::index_sequence<I...>) {
return (((static_cast<long>(I) + 1) * std::get<I>(t)) + ...);
}
template <typename... A>
long agirlikli_toplam(A... a) {
auto t = std::make_tuple(a...);
return agirlikli(t, std::index_sequence_for<A...>{});
}
int main() {
auto t = std::make_tuple(2, 3, 4);
long duz = std::apply([](auto... e) { return (0L + ... + e); }, t);
std::printf("apply %ld\n", duz);
std::printf("agirlikli %ld\n", agirlikli_toplam(2, 3, 4));
using ikinci = std::tuple_element_t<1, std::tuple<int, double, char>>;
std::printf("tip %d\n", std::is_same_v<ikinci, double>);
return 0;
}
apply 9 agirlikli 20 tip 1
MP18. std::apply demeti açar ve katlama ifadesi 2 + 3 + 4 toplamını verir. agirlikli
her elemanı konum numarasıyla çarpar: 1*2 + 2*3 + 3*4. index_sequence_for<A...> paketle
aynı uzunlukta bir dizi üretir ve I... bu diziyle A... ile birlikte açılır. Üçüncü
satırda tuple_element_t<1, ...> paketten ikinci tipi seçer; double olması beklenir
ve sorgu bunu doğrular. Hepsi derlemede çözülür, her farklı eleman sayısı için ayrı bir
gövde yazdırır, yani yine derleme sınıfındadır.
Paketin sınırları da var. Boş bir std::tuple<> geçerlidir, ama boş pakette [0] gibi bir
konum yoktur. Paket açma ancak bir kalıbın içinde yapılabilir; çıplak x... tek başına
bir ifade değildir. Bir sınıf şablonunda paket parametresi son
parametre olmalıdır. İşlev şablonunda paketin ardından parametre gelebilir, ama o parametre
çıkarılabilmeli ya da varsayılan bir argüman taşımalıdır; template <typename... A, typename B> gibi bir sıra, B çıkarılamıyorsa kullanılamaz. Özyineli açmada paketin bir
ilk elemanla (Ilk, Kalan...) birlikte yazılması ise başka bir nedene dayanır: ilk elemanı
ayırıp geri kalanla özyinelemek.
Sınırlayıcı Ölçüm
Gövde sayısı çağrı sayısına değil, tip listesine bağlıdır. Aynı tip listesiyle ikinci bir çağrı mevcut gövdeleri kullanır; farklı tipli bir çağrı yenilerini üretir.
// paket-yeniden.cpp — ayni tip listesi govdeyi yeniden kullanir
#include <cstdio>
#include "toplam.hpp"
int main() {
topla_oz(1, 2, 3);
topla_kat(1, 2, 3);
std::printf("ilk ozyineli %d katlama %d\n", oz_govde, kat_govde);
topla_oz(4, 5, 6);
topla_kat(4, 5, 6);
std::printf("ikinci ozyineli %d katlama %d\n", oz_govde, kat_govde);
topla_oz(1, 2, 3L);
topla_kat(1, 2, 3L);
std::printf("yeni tipli ozyineli %d katlama %d\n", oz_govde, kat_govde);
return 0;
}
ilk ozyineli 4 katlama 1 ikinci ozyineli 4 katlama 1 yeni tipli ozyineli 7 katlama 2
MP19. (4, 5, 6) çağrıları da int, int, int listesidir: gövde sayıları değişmez,
dörtten dörde. (1, 2, 3L) ise yeni bir tip listesidir: <int,int,long>, <int,long> ve
<long> üç yeni özyineli gövde doğurur, dörtten yediye çıkar; taban işlevi aynı kalır.
Katlama tarafında da yeni liste bir yeni gövde ekler. Bu yüzden sayı ölçünün kendisi
değildir; bedelin hangi sınıfa yazıldığını gösteren kanıttır: özyineli açma tip çeşitliliği arttıkça yeni gövde
yazdırır, ve bu bedel derleme sınıfında kalır.
Özet
- Bu dersin ölçtüğü düzenek bedeli derleme sınıfına taşır: bir paketi açmak, açma yöntemine göre farklı sayıda gövde üretir.
typename... Tbir parametre paketidir;sizeof...(T)eleman sayısını derlemede verir,x...paketi açar.- Özyineli açma her adımda ayrı bir şablon gövdesi ve bir taban durumu ister
(
ozyineli=4); katlama ifadesi aynı iş için tek gövde yeter (katlama=1). - Tekli katlama boş paketi yalnız
&&,||ve,ile kabul eder; ikili katlama başlangıç değeri taşır. - Aynı tip listesi gövdeyi yeniden kullanır (4 → 4); yeni bir tip listesi yeni gövdeler doğurur (4 → 7).
Sonraki Adım
Bu derste paket açma, tip listesine göre gövde yazdırdı ve bedel derlemede kaldı.
Sıradaki ders, tipin kendisine soru soran araçları, tip özelliklerini ele alıyor: bir
şablonun içinde if constexpr ile yapılan seçim koşumda kaç sorgu bırakıyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.