İçeriğe geç
academia.sh

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_cast ile 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; variant ile 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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat