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
inlineolmalı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.