İçeriğe geç
academia.sh

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... T bir 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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat