Ders 06 / 12
Statik Çok Biçimlilik
CRTP ile derleme zamanında gönderim, sanal işlevli sürümle karşıtlık, politika sınıfları ve mixin, kavramla kısıtlanmış statik arayüz ve variant ile koşuma dönen seçim.
Yayın Güncelleme
Bu derste
- CRTP ile bir taban sınıf şablonundan türeyen sınıfa derleme zamanında gönderimi kurar.
- Statik ve dinamik çok biçimliliğin bedelini hangi sınıfta ödediğini ayırt eder.
- Politika sınıfı ve mixin ile davranışı derleme zamanında bileşenlere böler.
- Farklı CRTP sınıflarının ortak tabanla tek koleksiyona konamayacağını ve variant ile seçimin koşuma döndüğünü gösterir.
İçindekiler
Önceki ders aday elemesini derlemeye taşıdı: uygun olmayan şablon hata vermeden listeden düştü. Bu ders aynı fikri bir sınıf hiyerarşisine uyguluyor. Bir taban sınıf, hangi türemiş sınıfla çalışacağını derleme sırasında biliyorsa, çağrıyı çalışırken seçmeye gerek kalmaz. Bu statik çok biçimliliktir. Sorumuz, bu seçimin bedelini kimin ödediği: karar derlemede verildiğinde derleyiciye ne eklenir, koşumdan ne düşer?
M11/K04, dinamik çok biçimliliği sanal işlevlerle ele aldı: tek bir arayüz, çalışırken seçilen gövde. M10/K03’te Rust’ta görülen “her tip için ayrı gövde” de aynı kavramdır; bu ders iki dili karşılaştırmaz. Bu ders bir üstünlük de kurmaz. İki yöntem de tek bir çağrı için farklı bir yerde ödeme yapar. Statik tarafta her türemiş tip için ayrı bir gövde yazılır ve bu bedel derleme sınıfındadır; dinamik tarafta gövdeleri derleyici tip başına üretmez, yalnız yazılan türemiş sınıflar kadar gövde vardır ve seçim çalışırken yapılır.
CRTP: Tabanın Türemiş Sınıfı Bilmesi
CRTP (Curiously Recurring Template Pattern, tuhaf biçimde yinelenen şablon örüntüsü)
şunu yapar: taban sınıf bir sınıf şablonudur ve parametresi türemiş sınıfın kendisidir.
Kare, Sekil<Kare>’den türer. Taban, static_cast<const D&>(*this) ile kendini türemiş
tipe çevirir ve türemişin işlevini çağırır. Sanal işlev yoktur: hangi gövdenin
çağrılacağı derlemede bellidir.
// sekil.hpp — CRTP tabani ve iki turemis sinif
#ifndef SEKIL_HPP
#define SEKIL_HPP
inline int govde = 0;
template <typename D>
struct Sekil {
long alan() const {
static const bool kayit = (++govde, true);
(void)kayit;
return static_cast<const D&>(*this).alan_hesapla();
}
};
struct Kare : Sekil<Kare> {
long kenar;
explicit Kare(long k) : kenar(k) {}
long alan_hesapla() const { return kenar * kenar; }
};
struct Dikdortgen : Sekil<Dikdortgen> {
long en;
long boy;
Dikdortgen(long e, long b) : en(e), boy(b) {}
long alan_hesapla() const { return en * boy; }
};
#endif
// crtp.cpp — crtp ile gonderim
#include <cstdio>
#include "sekil.hpp"
int main() {
Kare a(3);
Dikdortgen b(2, 5);
Kare c(4);
long toplam = a.alan() + b.alan() + c.alan();
std::printf("toplam=%ld govde=%d\n", toplam, govde);
return 0;
}
toplam=35 govde=2
MP32. Toplam 9 + 10 + 16 = 35 olur. Sekil<D>::alan iki farklı tip için
somutlaştırıldığından iki gövde vardır: Sekil<Kare>::alan ve Sekil<Dikdortgen>::alan.
Üçüncü çağrı (Kare(4)) ilk gövdeyi yeniden kullanır. Çekirdekteki crtp ile gonderim
satırı bu sayıdır: çağrı sayısı üç, gövde sayısı iki. Bedel derleme sınıfına taşınmıştır
ve yanında ürün sayısını taşır.
Bu düzende Sekil<D> tek bir sınıf değil bir aileye ait ayrı sınıflardır; Sekil<Kare> ile
Sekil<Dikdortgen> arasında bir türetme ilişkisi yoktur. static_cast<const D&>(*this)
yalnız nesne gerçekten bir D ise doğrudur. D, Sekil<D>’den hiç türemiyorsa dönüşüm
derlemede reddedilir. Ama bir sınıf Sekil<B>’den türeyip B olarak başka bir türemiş
sınıfı yazarsa, derleyici bunu yakalayamaz: nesne gerçekte o tip değildir ve davranış
tanımsızdır. Bu yüzden taban sınıfın yapıcısını
korumalı yapmak ve türemişi final işaretlemek yaygın savunmalardır.
Sanal İşlevli Karşılığı
Aynı hesabı sanal işlevle yazalım. Tek bir taban sınıf ve iki türemiş sınıf vardır; gövdeler bir kez yazılır ve çağrı taban referansı üzerinden, çalışırken seçilir. Bu seçim genellikle sanal işlev tablosu denen bir bellek yapısıyla gerçeklenir; burada boyutunu ya da içeriğini basmıyoruz, çünkü ölçtüğümüz şey gövde ve çağrı sayısıdır.
// sanal.cpp — sanal islevle ayni hesap
#include <cstdio>
#include <memory>
#include <vector>
inline int sanal_govde = 0;
inline int dolayli_cagri = 0;
struct SanalSekil {
virtual ~SanalSekil() = default;
virtual long alan() const = 0;
};
struct SanalKare : SanalSekil {
long kenar;
explicit SanalKare(long k) : kenar(k) {}
long alan() const override {
static const bool kayit = (++sanal_govde, true);
(void)kayit;
return kenar * kenar;
}
};
struct SanalDikdortgen : SanalSekil {
long en;
long boy;
SanalDikdortgen(long e, long b) : en(e), boy(b) {}
long alan() const override {
static const bool kayit = (++sanal_govde, true);
(void)kayit;
return en * boy;
}
};
int main() {
std::vector<std::unique_ptr<SanalSekil>> v;
v.push_back(std::make_unique<SanalKare>(3));
v.push_back(std::make_unique<SanalDikdortgen>(2, 5));
v.push_back(std::make_unique<SanalKare>(4));
long toplam = 0;
for (const auto& s : v) {
++dolayli_cagri;
toplam += s->alan();
}
std::printf("toplam=%ld govde=%d dolayli=%d\n", toplam, sanal_govde, dolayli_cagri);
return 0;
}
toplam=35 govde=2 dolayli=3
MP33. Sonuç aynı, toplam 35 ve gövde sayısı yine iki; çünkü iki türemiş sınıf iki gövde
yazar. Fark, gövdelerin nasıl seçildiğindedir: burada üç çağrının üçü de taban referansı
üzerinden dolaylıdır (dolayli=3) ve hangi gövdenin çalışacağı çalışırken belirlenir. CRTP
sürümünde üç çağrının hiçbiri dolaylı değildir. Bu iki satır sınıfları ayırır: dolaylı
çağrı sayısı koşum, her tip için ayrı gövde derleme sınıfına yazılır. İkisi de bir
ürün sayısıdır; hangisinin daha hızlı olduğu bu derste iddia edilmez.
Bileşenlere Bölmek: Politika ve Mixin
CRTP’nin ikinci kullanımı, bir sınıfa davranış eklemektir. Mixin adı verilen bu kullanımda taban sınıf şablonu türemiş sınıfa bir işlev ekler; türemiş sınıf tabanı seçerek kendi arayüzünü kurar. Politika sınıfı ise bir şablon parametresi olarak verilen ve davranışın bir parçasını belirleyen sınıftır. Her ikisi de seçimi derlemede yapar.
// mixin.cpp — mixin ve politika sinifi
#include <cstdio>
template <typename D>
struct Yazdirilabilir {
void yaz() const {
std::printf("%s alan=%ld\n", D::ad(), static_cast<const D&>(*this).alan());
}
};
struct MetreOlcu {
static long cevir(long v) { return v * 100; }
};
struct SantimOlcu {
static long cevir(long v) { return v; }
};
template <typename Olcu>
struct Kare : Yazdirilabilir<Kare<Olcu>> {
long kenar;
explicit Kare(long k) : kenar(k) {}
static const char* ad() { return "kare"; }
long alan() const { return Olcu::cevir(kenar) * Olcu::cevir(kenar); }
};
int main() {
Kare<SantimOlcu> a(3);
Kare<MetreOlcu> b(3);
a.yaz();
b.yaz();
return 0;
}
kare alan=9 kare alan=90000
MP34. Yazdirilabilir<D> yaz işlevini ekler; Kare<Olcu> hem mixin tabanı hem de
politika parametresi alır. SantimOlcu ile alan 9, MetreOlcu ile 90000 çıkar. Her
Olcu seçimi ayrı bir Kare ve ayrı bir yaz gövdesi yazdırır. Politika ne kadar çok
bileşene bölünürse gövde sayısı, kullanılan bileşim sayısı kadar çarpılabilir; bu bileşimin bedeli derleme sınıfında
birikir ve çalışırken bir seçim kalmaz.
Kavramla Kısıtlanmış Statik Arayüz
CRTP’de arayüz örtüktür: taban alan_hesapla çağırır, ama türemiş sınıfta bu işlev yoksa
hata taban sınıf gövdesinin içinde çıkar ve derleyiciler onu genellikle uzun bir
somutlaştırma iletisi olarak gösterir. C++20
kavramları arayüzü açıkça yazar. Aşağıdaki işlev, alan() çağrısı geçerli olan ve
sonucu long’a dönüşebilen her tipi kabul eder ve çağrı noktasında kısıtı denetler.
// arayuz.cpp — kavramla kisitlanmis statik arayuz
#include <concepts>
#include <cstdio>
#include "sekil.hpp"
template <typename T>
concept AlanliSekil = requires(const T& t) {
{ t.alan() } -> std::convertible_to<long>;
};
template <AlanliSekil T>
long ikiyle(const T& s) { return s.alan() * 2; }
struct Alansiz {};
static_assert(AlanliSekil<Kare>);
static_assert(AlanliSekil<Dikdortgen>);
static_assert(!AlanliSekil<Alansiz>);
int main() {
Kare k(3);
Dikdortgen d(2, 5);
std::printf("ikiyle %ld %ld\n", ikiyle(k), ikiyle(d));
return 0;
}
ikiyle 18 20
MP35. Kare ve Dikdortgen kavramı alan üyesi üzerinden sağlar; Alansiz sağlamaz ve
static_assert bunu derlemede doğrular. Kavram, çağrıyı yapan tarafa bir sözleşme
kazandırır: kısıt sağlanmazsa hata çağrı noktasında çıkar ve derleyiciler genellikle kısıtın adını
gösterir. Ek olarak,
kavram bir ürün sayısı eklemez; denetim derleme sırasında biter ve çalışan koda hiçbir şey
yazmaz.
Varsayılan Davranış ve Gizleme
Sanal işlevde türemiş sınıf, taban sınıfın bir işlevini aynı imzayla yeniden tanımlar;
CRTP’de ise düzen farklıdır. Tabanda varsayılan bir gövde yazılır, türemiş sınıf aynı
adla bir işlev tanımlarsa onu gizler ve taban işlev, çağrıyı türemişe yönlendirdiği için
türemişin gövdesini görür. Her şey derlemede çözülür: çağrı hangi adın kapsamından
gelirse o gövde seçilir. Aşağıda Betimli tabanı varsayılan bir betimle yazar; Daire
kendi sürümünü tanımlar ve tabandaki çağrı ona gider.
// varsayilan.cpp — tabanda varsayilan, turemiste gizleme
#include <cstdio>
template <typename D>
struct Betimli {
const char* betimle() const {
return static_cast<const D&>(*this).ad();
}
const char* ad() const { return "sekil"; }
};
struct Ucgen : Betimli<Ucgen> {};
struct Daire : Betimli<Daire> {
const char* ad() const { return "daire"; }
};
int main() {
Ucgen u;
Daire d;
std::printf("ucgen %s\n", u.betimle());
std::printf("daire %s\n", d.betimle());
return 0;
}
ucgen sekil daire daire
MP36. Ucgen kendi ad işlevini tanımlamadığı için tabanın varsayılanı bulunur ve
betimle sekil döndürür. Daire aynı adla bir işlev tanımladığı için
static_cast<const D&> üzerinden daire bulunur. İkisi de derlemede çözülür; yani tabandaki varsayılan,
yalnız türemiş sınıfın yazmadığı yer için gövde yazdırır. Çalışırken yapılan seçimin yerine burada
derlemedeki ad araması çalışır.
Her Tip İçin Ayrı Statik Üye
Sınıf şablonunun her somutlaştırması kendi statik üyesini taşır. CRTP’de bu, her türemiş
sınıfa kendi sayacını vermek için kullanılır: sayaç tabanın içinde durur, ama Sayilan<Kare>
ile Sayilan<Dikdortgen> ayrı sınıflar olduğu için sayaçlar da ayrıdır. Şablon
olmayan bir tabanda tek bir sayaç bütün türemişleri toplardı.
// sayilan.cpp — tip basina sayac
#include <cstdio>
template <typename D>
struct Sayilan {
static inline int adet = 0;
Sayilan() { ++adet; }
Sayilan(const Sayilan&) { ++adet; }
~Sayilan() { --adet; }
};
struct Kare : Sayilan<Kare> {};
struct Dikdortgen : Sayilan<Dikdortgen> {};
int main() {
Kare a;
Kare b;
Dikdortgen c;
std::printf("kare=%d dikdortgen=%d\n", Sayilan<Kare>::adet, Sayilan<Dikdortgen>::adet);
{
Kare gecici = a;
std::printf("kare=%d\n", Sayilan<Kare>::adet);
}
std::printf("kare=%d\n", Sayilan<Kare>::adet);
return 0;
}
kare=2 dikdortgen=1 kare=3 kare=2
MP37. İki Kare ve bir Dikdortgen oluşturulunca sayaçlar ayrı sayılır: iki ve bir.
Kopya, kapsama girince sayacı üçe çıkarır, kapsamdan çıkınca iki olur. Ortak sayaç
isteyen bir tasarım için tabanın şablon olmaması gerekir; ayrı sayaç istendiğinde
CRTP bunu tek satırla verir. Bu davranış her türemiş sınıf için ayrı bir sayaç nesnesi
yazılması demektir ve bedel derleme sınıfına yazılır, koşumda yalnız sayaç artışı kalır.
Hangisi Ne Zaman
Bu karşılaştırma bir sıralama değil bir kılavuzdur. Tip kümesi derlemede kapalıysa ve her
tip için gövdeler derleyiciye yazdırılabiliyorsa CRTP, gönderimi dolaylı çağrısız kurar;
ama farklı tipleri tek bir koleksiyonda taşımaz. Tip kümesi çalışırken büyüyorsa ya da
farklı tipler tek bir kapta tutulacaksa sanal işlevli sürüm gerekir; gövdeler tip başına
üretilmez ama her çağrı çalışırken seçilir. Kapalı ama karışık bir küme için std::variant
ikisinin arasında durur: tek koleksiyon verir, seçimi koşuma bırakır. Üç yöntem de aynı
alan toplamını verdi (35); fark, sayının hangi sınıfta ödendiğidir.
Sınırlayıcı Ölçüm
CRTP’nin sınırı, türemiş sınıfların ortak bir tabanının olmamasıdır. Sekil<Kare> ile
Sekil<Dikdortgen> ayrı tiplerdir; birinin işaretçisi diğerinin işaretçisine dönüşmez.
Bu yüzden Kare ve Dikdortgen tek bir koleksiyona CRTP ile konamaz. Tek koleksiyon
gerekiyorsa, kapalı bir tip kümesi için std::variant<Kare, Dikdortgen> ve std::visit
kullanılır. Hangi alternatifin tutulduğu çalışırken bir indekste durur, yani seçim koşuma
döner.
// degisken.cpp — variant ile tek koleksiyon
#include <cstdio>
#include <variant>
#include <vector>
#include "sekil.hpp"
int main() {
std::vector<std::variant<Kare, Dikdortgen>> v;
v.emplace_back(Kare(3));
v.emplace_back(Dikdortgen(2, 5));
v.emplace_back(Kare(4));
int secim = 0;
long alan = 0;
for (const auto& s : v) {
++secim;
alan += std::visit([](const auto& x) { return x.alan(); }, s);
}
std::printf("secim=%d alan=%ld\n", secim, alan);
return 0;
}
secim=3 alan=35
MP38. Üç eleman, üç std::visit çağrısı: secim=3, toplam alan=35. Gövdeler yine
Sekil<Kare>::alan ve Sekil<Dikdortgen>::alan olarak derlenmiştir, ama hangisinin
çağrılacağı artık her eleman için çalışırken bir indekse bakılarak seçilir. Yani
variant, CRTP’nin derlemede aldığı kararı koşuma geri verir; bu sürümde bedel koşum
sınıfına kaydı. Tek koleksiyon, kapalı bir tip kümesi ve seçimin ödenmesi birlikte gelir.
Küme açık olmalıysa (sonradan yeni tipler eklenecekse) sanal işlevli sürüm uygundur.
Özet
- Bu dersin ölçtüğü düzenek bedeli derleme sınıfına taşır: CRTP ile üç çağrı iki
gövdeyle yapılır (
toplam=35 govde=2) ve hiçbiri dolaylı değildir. - CRTP’de taban sınıf şablonu türemiş sınıfı bilir; çağrı
static_castile türemişe gider. - Sanal işlevli sürüm aynı toplamı verir, ama üç çağrının üçü de çalışırken seçilir
(
dolayli=3); iki yöntem farklı sınıflarda öder, üstünlük kurulmaz. - Mixin ve politika sınıfları davranışı derlemede bileşenlere böler; her bileşim ayrı gövde yazdırır. Kavram, statik arayüzü açıkça ve çağrı noktasında denetler.
- Farklı CRTP sınıfları ortak bir tabanla tek koleksiyona konamaz;
variantile seçim koşuma döner (secim=3 alan=35).
Sonraki Adım
Bu derste statik gönderim, hangi gövdenin çalışacağını derlemeye taşıdı; bedel gövde sayısında ve yazılan koda yansıdı. Ama tek bir iş parçacığında kalmak şart değil: kilit ile korunan bir sayaç, her güncellemede kaç eşgüdüm noktası ekliyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.