İçeriğe geç
academia.sh

Ders 02 / 12

Şablon Özelleştirme

Tam ve kısmi özelleştirme, genel şablon, en özel olanın seçilmesi, bool paketinin bellek düzeni ve bitten okumanın koşuma bıraktığı bedel.

Yayın Güncelleme

Bu derste

  • Tam özelleştirme ile kısmi özelleştirmeyi genel şablondan ayırt eder.
  • Birden çok özelleştirme uyduğunda en özel olanın seçildiğini gösterir.
  • Paket<bool> özelleştirmesinin yuva sayısını işaretçi eşitliğiyle ölçer.
  • Bitten okumanın bedelini bellekten koşuma geri döndüren adımla açıklar.
İçindekiler

Önceki ders, bir şablonun her yeni tip için yeni bir gövde yazdırdığını ve bu bedelin derleme sınıfında kaldığını gösterdi. Şablonun ürettiği gövde her tip için aynı kalıptır; ama bazı tipler için kalıp yanlış ya da savurgan olabilir. Bu ders o durumda kalıbın elle değiştirilmesini, yani özelleştirmeyi ele alıyor. Soru değişmedi: bu düzenek bedeli nereye taşıdı? Burada yanıt bir gövdenin sayısı değil, verinin bellekte kaç yuvaya yayıldığıdır.

Genel Şablon ve Tam Özelleştirme

Sekiz değer tutan bir Paket<T> düşünelim. Genel şablon her değer için bir yuva ayırır: T yuva[8]. bool için bu israftır, çünkü bir değer tek bit taşır ama bir bayt tutar. Tam özelleştirme (template <>) belirli bir tip için genel şablonu tümüyle değiştirir; burada Paket<bool> sekiz değeri tek bir unsigned char içinde tutar. Arayüz aynı kalır, gövde değişir.

// paket.hpp — genel sablon ve bool icin tam ozellestirme
#ifndef PAKET_HPP
#define PAKET_HPP

template <typename T>
struct Paket {
    T yuva[8] = {};
    const void* yer(int i) const { return &yuva[i]; }
    void yaz(int i, T deger) { yuva[i] = deger; }
    T oku(int i, int&) const { return yuva[i]; }
};

template <>
struct Paket<bool> {
    unsigned char bitler = 0;
    const void* yer(int) const { return &bitler; }
    void yaz(int i, bool deger) {
        if (deger) bitler = static_cast<unsigned char>(bitler | (1u << i));
        else bitler = static_cast<unsigned char>(bitler & ~(1u << i));
    }
    bool oku(int i, int& adim) const {
        adim += 2;   // bir kaydirma, bir maske
        return ((bitler >> i) & 1) != 0;
    }
};

#endif

yer bir değerin hangi yuvada durduğunu gösterir; yuvaları işaretçi eşitliğiyle sayacağız, adres basmadan. İki işaretçi eşitse iki değer aynı yuvadadır. Aşağıdaki program her iki paketi aynı kalıpla doldurur, okur ve ayrı yuva sayısını yazar.

// ozellestirme.cpp — ayni arayuz, farkli yuva sayisi
#include <cstdio>

#include "paket.hpp"

template <typename T>
static int ayri_yuva(const Paket<T>& p) {
    int ayri = 0;
    for (int i = 0; i < 8; ++i) {
        bool yeni = true;
        for (int j = 0; j < i; ++j)
            if (p.yer(i) == p.yer(j)) yeni = false;
        if (yeni) ++ayri;
    }
    return ayri;
}

template <typename T>
static void dene(const char* ad, Paket<T>& p) {
    int adim = 0;
    p.yaz(1, true);
    p.yaz(6, true);
    std::printf("%s ", ad);
    for (int i = 0; i < 8; ++i) std::printf("%d", static_cast<int>(p.oku(i, adim)));
    std::printf(" yuva=%d\n", ayri_yuva(p));
}

int main() {
    Paket<int> genel;
    Paket<bool> bitli;
    dene("int ", genel);
    dene("bool", bitli);
    return 0;
}
int  01000010 yuva=8
bool 01000010 yuva=1

MP6. İki paket aynı değerleri aynı sırayla okur; fark yalnız yuva sayısındadır: Paket<int> sekiz ayrı yuva, Paket<bool> tek yuva kullanır. Çekirdekteki bool ozellestirmesi satırının int=8 bool=1 sayıları bu ölçümdür. Bedel bellek sınıfına düşer: aynı iş, verinin yerleşimine göre farklı sayıda depolama yuvasına dokunur.

Kısmi Özelleştirme ve En Özel Olanın Seçilmesi

Tam özelleştirme tipleri tek tek adlandırır. Kısmi özelleştirme ise parametrelerin bir desenini yakalar: “iki parametre aynı tipse” ya da “tip bir işaretçiyse” gibi. İki parametreli bir Cift<T, U> için genel şablon ayrı tipleri, Cift<T, T> kısmi özelleştirmesi aynı tipleri, Cift<int, int> tam özelleştirmesi ise yalnız int çiftini yakalar. Bir kullanımda birden çok özelleştirme uyuyorsa derleyici en özel olanı seçer.

// cift.hpp — genel sablon, kismi ve tam ozellestirme
#ifndef CIFT_HPP
#define CIFT_HPP

template <typename T, typename U>
struct Cift {
    static const char* tur() { return "farkli"; }
};

template <typename T>
struct Cift<T, T> {
    static const char* tur() { return "ayni"; }
};

template <>
struct Cift<int, int> {
    static const char* tur() { return "tamsayi"; }
};

#endif
// cift-sec.cpp — hangi ozellestirme secildi
#include <cstdio>

#include "cift.hpp"

int main() {
    std::printf("int,double    %s\n", Cift<int, double>::tur());
    std::printf("double,double %s\n", Cift<double, double>::tur());
    std::printf("int,int       %s\n", Cift<int, int>::tur());
    return 0;
}
int,double    farkli
double,double ayni
int,int       tamsayi

MP7. Cift<int, int> hem kısmi desene (T, T) hem tam özelleştirmeye uyar; derleyici tam olanı seçer. Cift<double, double> yalnız kısmi desene uyar. Seçim derlemede biter ve koşumda karar adımı bırakmaz. Bir kural daha var: özelleştirme, o tipin ilk somutlaştırmasından önce görünür olmalıdır; tip zaten genel şablondan üretildikten sonra gelen bir özelleştirme programı hatalı kılar ve derleyicinin bunu bildirmesi gerekmez. İşlev şablonları ise kısmi özelleştirilemez; aynı iş için aşırı yükleme kullanılır; bunu Aşırı Yükleme Çözümlemesi dersi işler.

İşaretçi Deseni ve Özelleştirmenin Sınırı

Kısmi özelleştirme bir tipin biçimini de yakalayabilir. Ac<T*>, T ne olursa olsun işaretçi olan her tipi yakalar; genel şablon ise değeri olduğu gibi döndürür. Burada bir sınır görünür: bir özelleştirme genel şablonun üyelerini devralmaz. Özelleştirme kendi başına yeni bir sınıf tanımıdır; genel şablonda bulunan ve çağrılmasını beklediğimiz her üyeyi onda yeniden yazmak gerekir. Arayüzün ortak kalması bir derleyici garantisi değil, yazarın üstlendiği bir sözleşmedir. Paket<bool>’un yer, yaz ve oku üyelerini genel şablonla aynı adlarla taşıması bu sözleşmenin uygulanmasıdır.

// isaretci.hpp — kismi ozellestirme isaretci desenini yakalar
#ifndef ISARETCI_HPP
#define ISARETCI_HPP

template <typename T>
struct Ac {
    static const char* ad() { return "deger"; }
    static T al(const T& x) { return x; }
};

template <typename T>
struct Ac<T*> {
    static const char* ad() { return "isaretci"; }
    static T al(T* const& p) { return *p; }
};

#endif
// isaretci-sec.cpp — ayni cagri, iki ayri govde
#include <cstdio>

#include "isaretci.hpp"

int main() {
    int x = 7;
    int* p = &x;
    std::printf("%s %d\n", Ac<int>::ad(), Ac<int>::al(x));
    std::printf("%s %d\n", Ac<int*>::ad(), Ac<int*>::al(p));
    return 0;
}
deger 7
isaretci 7

MP8. Ac<int*> genel şablondan değil, işaretçi desenini yakalayan kısmi özelleştirmeden gelir ve değeri işaretçinin gösterdiği yerden okur. İki kullanım da yedi yazdırır, ama arkalarındaki gövdeler farklıdır; hangisinin kullanılacağı derlemede belirlenir ve koşumda sorgu kalmaz. Bu dersin sınıfı yine de bellek olarak kalır: bellek ölçümü yalnız Paket<bool> satırıdır ve bu bölüm, özelleştirmenin sınırını gösteren yardımcı bir örnektir.

İşlev Şablonunun Tam Özelleştirmesi

İşlev şablonları kısmi özelleştirilemez, ama tam özelleştirilebilir. Burada başlıkla ilgili bir tuzak var: tam özelleştirme artık bir şablon değil, sıradan bir işlevdir, bu yüzden başlıkta tanımlanıyorsa inline olmalıdır; aksi halde başlığı içeren her birim aynı işlevi tanımlar ve bağlayıcı çift tanım görür. Aşağıdaki etiket genel şablonu her tipe diger der, int için tam özelleştirme tamsayi der.

// etiket.hpp — islev sablonu ve inline tam ozellestirme
#ifndef ETIKET_HPP
#define ETIKET_HPP

template <typename T>
const char* etiket() { return "diger"; }

template <>
inline const char* etiket<int>() { return "tamsayi"; }

#endif
// etiket-sec.cpp — hangi etiket secildi
#include <cstdio>

#include "etiket.hpp"

int main() {
    std::printf("%s %s %s\n", etiket<double>(), etiket<int>(), etiket<long>());
    return 0;
}
diger tamsayi diger

MP9. etiket<int> özelleştirmeye, etiket<double> ve etiket<long> genel şablona gider; genel şablondan iki ayrı gövde, özelleştirmeden bir gövde doğar. Burada da seçim derlemede biter. İşlev şablonlarında özelleştirme yerine çoğu zaman aşırı yükleme tercih edilir, çünkü aşırı yükleme çözümlemesi özelleştirmeden farklı kurallarla aday seçer; farkı Aşırı Yükleme Çözümlemesi dersi gösterir.

std::vector<bool> ve Vekil Nesne

Standart kütüphane bu fikri kendisi kullanır: std::vector<bool> bir özelleştirmedir ve standart onun için değerleri bitlere sıkıştıran, yer kazandıran bir gösterim önerir; yaygın gerçeklemeler bunu Paket<bool> gibi yapar. Sonucun bir bedeli var: bir elemanın adresini almak mümkün değildir, çünkü eleman ayrı bir bool nesnesi değildir. Bu yüzden operator[] elemanın kendisini değil, o biti okuyup yazan bir vekil nesne döndürür. Elemanın adresini almayı denemek koşturulmaz; tanımlı karşılığı vekil nesneyle çalışmaktır.

// vekil.cpp — vector<bool> elemanina vekil nesne uzerinden erisim
#include <cstdio>
#include <vector>

int main() {
    std::vector<bool> v(8, false);
    v[3] = true;      // operator[] bir vekil nesne dondurur, atama biti yazar
    bool uc = v[3];   // vekil nesne bool'a donusur

    int dolu = 0;
    for (std::size_t i = 0; i < v.size(); ++i) dolu += v[i];
    std::printf("dolu %d, v[3] %d\n", dolu, uc ? 1 : 0);
    return 0;
}
dolu 1, v[3] 1

Bitlerin sıkıştırılması bellekten kazanılan bedeli bir yere geri öder. Nereye, bir sonraki bölümde ölçülüyor.

En Özel Özelleştirme Kazanır

Aynı tip için birden çok kısmi özelleştirme eşleşebilir. Derleyici, kalıbı diğerlerinden daha belirli olanı seçer: T* kalıbı const T* kalıbından daha geneldir, çünkü const int* ikisine de uyar ve ikinci kalıp birincinin bir alt kümesidir. Hiçbiri diğerinden daha belirli değilse kullanım belirsizdir ve derleme durur; bunu düzyazıda bırakıyorum, çünkü derlenmeyen kod blok olarak yazılmaz. Tam özelleştirme her zaman kısmi olandan önce gelir.

// en-ozel.cpp — en belirli kalip secilir
#include <cstdio>

template <typename T>
struct Tur { static const char* ad() { return "genel"; } };

template <typename T>
struct Tur<T*> { static const char* ad() { return "isaretci"; } };

template <typename T>
struct Tur<const T*> { static const char* ad() { return "sabite isaretci"; } };

template <>
struct Tur<int*> { static const char* ad() { return "int isaretci"; } };

int main() {
    std::printf("int          %s\n", Tur<int>::ad());
    std::printf("double*      %s\n", Tur<double*>::ad());
    std::printf("const char*  %s\n", Tur<const char*>::ad());
    std::printf("int*         %s\n", Tur<int*>::ad());
    return 0;
}
int          genel
double*      isaretci
const char*  sabite isaretci
int*         int isaretci

MP10. int hiçbir kalıba uymaz ve genel şablon kalır. double* yalnız T* kalıbına uyar. const char* hem T* hem const T* kalıbına uyar ve ikincisi daha belirlidir. int* için tam özelleştirme vardır ve kısmi olanı yener. Bu seçim derlemede biter: her kullanılan tip için bir sınıf tanımı seçilir ve koşumda sorgu bırakmaz.

İşlev Şablonları Kısmen Özelleştirilemez

Kısmi özelleştirme yalnız sınıf şablonlarında vardır; işlev şablonunun yalnız tam özelleştirmesi yazılabilir. İşlev tarafında kısmi eşdeğeri aşırı yüklemedir: T ve T* için iki ayrı işlev şablonu yazılır ve aşırı yükleme çözümlemesi hangisinin daha belirli olduğuna karar verir. Bu karar bir sonraki derslerin konusudur; burada yalnız sonucu görmek yeter. İşlev şablonlarının tam özelleştirmesi ise çoğunlukla aşırı yüklemeyle değiştirilir, çünkü özelleştirmeler aşırı yükleme çözümlemesine katılmaz: derleyici önce genel şablonlar arasından seçer, ardından seçilen şablonun özelleştirmesini kullanır.

// islev-yukleme.cpp — islev sablonunda kismi yerine yukleme
#include <cstdio>

template <typename T>
const char* tanit(T) { return "deger"; }

template <typename T>
const char* tanit(T*) { return "isaretci"; }

int main() {
    int n = 5;
    std::printf("tanit(n)  %s\n", tanit(n));
    std::printf("tanit(&n) %s\n", tanit(&n));
    return 0;
}
tanit(n)  deger
tanit(&n) isaretci

MP11. tanit(n) için yalnız T şablonu uygundur. tanit(&n) için ikisi de uygundur ve T* şablonu daha belirlidir, bu yüzden seçilir. Kısmi özelleştirmenin sınıf tarafında yaptığı iş, işlev tarafında aşırı yükleme çözümlemesinin yaptığı işe döner; her ikisinde de seçim derlemede biter ve bedel derleme sınıfındadır.

Sınırlayıcı Ölçüm

Paket<bool>, sekiz değeri tek yuvaya sığdırarak bellek bedelini düşürdü. Ama bir biti okumak için önce yuvayı kaydırmak, sonra maskelemek gerekir; Paket<int> ise değeri doğrudan okur. Sayaç, Paket<bool>::oku içinde iki adım (kaydırma ve maske) ve Paket<int>::oku içinde sıfır adım sayar. Aşağıdaki program her pakette sekiz okuma yapar.

// okuma-adimi.cpp — okuma basina adim
#include <cstdio>

#include "paket.hpp"

template <typename T>
static void say(const char* ad, const Paket<T>& p) {
    int adim = 0;
    int okuma = 0;
    for (int i = 0; i < 8; ++i) {
        p.oku(i, adim);
        ++okuma;
    }
    std::printf("%s okuma=%d adim=%d okuma_basina=%d\n", ad, okuma, adim, adim / okuma);
}

int main() {
    Paket<int> genel;
    Paket<bool> bitli;
    say("int ", genel);
    say("bool", bitli);
    return 0;
}
int  okuma=8 adim=0 okuma_basina=0
bool okuma=8 adim=16 okuma_basina=2

MP12. Paket<bool> her okumada iki adım öder, Paket<int> sıfır. Bellekten kazanılan bedel koşuma döndü: yuva sayısı sekizden bire indi, ama her okuma bir kaydırma ve maske istiyor. Bu dersin merkezi ölçümü bellek sınıfında kalır, çünkü çekirdekteki sayı yuva sayısıdır; okuma başına adım, bedelin tek yönlü kazanılmadığını gösteren sınırdır. Adım sayısı burada modeldir: kaydırma ve maske, sayaç satırlarıyla sayılmıştır, ölçülmüş bir süre değildir.

Özet

  • Bu dersin ölçtüğü düzenek bedeli bellek sınıfına taşır: Paket<bool> sekiz değeri tek yuvada tutar (int=8 bool=1).
  • Tam özelleştirme (template <>) tek bir tipin gövdesini değiştirir; kısmi özelleştirme bir parametre desenini yakalar; birden çok uyum varsa en özel olan seçilir.
  • Özelleştirme, tipin ilk somutlaştırmasından önce görünür olmalıdır; işlev şablonları kısmi özelleştirilemez, tam özelleştirmesi ise başlıkta inline olmalıdır.
  • Bir özelleştirme genel şablonun üyelerini devralmaz; Ac<T*> gibi bir desen kısmi özelleştirmeyle yakalanır ve arayüz sözleşmesini yazar korur.
  • std::vector<bool> elemanının adresi alınamaz; operator[] bir vekil nesne döndürür.
  • Bitten okuma her erişimde iki adım ister: bellekten kazanılan bedel koşuma geri döner.

Sonraki Adım

Bu derste özelleştirme bedeli yuvaya koydu, okuma başına adım da koşuma geri taşıdı. Sıradaki ders, şablonun parametre sayısını sabitten değişkene çeviriyor: bir parametre paketi açılırken derleyicinin kaç gövde ürettiği ölçülü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