İçeriğe geç
academia.sh

Ders 05 / 12

Aşırı Yükleme Çözümlemesi ve SFINAE

Aday ve uygun aday kavramı, şablon ile şablon olmayan işlevin yarışı, enable_if_t ile aday eleme, void_t ile algılama ve C++20 kavramlarıyla kısıtlama.

Yayın Güncelleme

Bu derste

  • Aşırı yükleme çözümlemesinde aday, uygun aday ve en iyi aday ayrımını yapar.
  • SFINAE ilkesini ve enable_if_t ile aday elemeyi uygular.
  • void_t ile bir ifadenin geçerli olup olmadığını algılayan tip özelliği yazar.
  • SFINAE'nin yalnız bildirimin hemen bağlamını kapsadığını, gövdedeki hatanın derlemeyi durdurduğunu açıklar.
İçindekiler

Önceki ders tipe bakıp dal seçmeyi gösterdi: karar bir if constexpr içinde veriliyordu. Bir adım önce bir soru daha var. Aynı adla birkaç işlev varsa derleyici hangisini çağırır ve uygun olmayan bir şablon aday listesinde ne olur? Bu ders çağrının nasıl seçildiğini ve şablonun yanlış tiple karşılaştığında hata vermeden nasıl elendiğini ölçüyor. Seçim derlemede biter ve bedel yine derleme sınıfındadır.

Aday, Uygun Aday, En İyi Aday

Bir işlev çağrısında derleyici üç aşamadan geçer. Önce çağrılan adla görünen tüm işlevleri ve işlev şablonlarını aday olarak toplar. Sonra argüman sayısı ve dönüşümü uygun olanları uygun aday listesine alır. Son olarak uygun adaylar arasında argümanlara en iyi uyan en iyi aday seçilir. Eşit iyilikte iki aday kalırsa ve hiçbir eşitlik kuralı birini öne çıkarmazsa çağrı belirsizdir ve derleme durur.

Şablon ile şablon olmayan işlev eşit iyilikte yarışırsa şablon olmayan kazanır. Ama şablon, argüman tipine tam uyum sağlarken şablon olmayan işlev bir dönüşüm gerektiriyorsa şablon kazanır, çünkü tam uyum dönüşümden iyidir.

// yukleme.cpp — sablon ve sablon olmayan aday
#include <cstdio>
#include <string>

const char* sec(int) { return "int"; }
const char* sec(double) { return "double"; }

template <typename T>
const char* sec(T) { return "sablon"; }

int main() {
    std::printf("sec(1)      %s\n", sec(1));
    std::printf("sec(1.5)    %s\n", sec(1.5));
    std::printf("sec('a')    %s\n", sec('a'));
    std::printf("sec(2.5f)   %s\n", sec(2.5f));
    std::printf("sec(string) %s\n", sec(std::string("x")));
    return 0;
}
sec(1)      int
sec(1.5)    double
sec('a')    sablon
sec(2.5f)   sablon
sec(string) sablon

sec(1) ve sec(1.5) için şablon olmayan işlev tam uyar ve eşitlikte kazanır. 'a' bir char’dır; int adayı bir tamsayı terfisi gerektirir, şablon ise T = char ile tam uyar, bu yüzden şablon kazanır. float için de aynıdır. string için yalnız şablon uygun adaydır.

SFINAE: Yerine Koyma Başarısızlığı Hata Değildir

Şablon argümanı çıkarıldıktan sonra derleyici çıkarılan tipi şablonun bildirimine koyar. Bu koyma geçersiz bir tip ya da ifade üretirse, o şablon aday listesinden sessizce düşer ve derleme hata vermez. İlke SFINAE adıyla bilinir: yerine koyma başarısızlığı hata değildir (substitution failure is not an error). Hata ancak hiçbir aday kalmazsa verilir.

SFINAE’yi kasıtlı kullanmanın klasik yolu std::enable_if_t<koşul, tip>’tir: koşul doğruysa tip’e eşittir, yanlışsa yerine koyma başarısız olur ve şablon elenir. Çoğunlukla tür olmayan bir şablon parametresinin tipine yerleştirilir ve o parametreye = 0 gibi bir varsayılan değer verilir.

// enable-if.cpp — enable_if_t ile aday eleme
#include <cstdio>
#include <type_traits>

template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
const char* tur(T) { return "tamsayi"; }

template <typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
const char* tur(T) { return "kayan"; }

int main() {
    std::printf("tur(7)   %s\n", tur(7));
    std::printf("tur(2.5) %s\n", tur(2.5));
    return 0;
}
tur(7)   tamsayi
tur(2.5) kayan

MP26. tur(7) için ikinci şablonun bildirimi enable_if_t<false, int> olur, yerine koyma başarısız olur ve ikinci aday elenir; geriye tamsayi kalır. tur(2.5) için tersi olur. İki şablonun imzası aynı görünür, ama şablon parametrelerinin tipleri farklı koşullara bağlı olduğundan imzalar farklıdır ve çakışma olmaz. Seçim derlemededir ve koşumda sorgu bırakmaz.

Dönüşüm Sıralaması

Uygun adaylar arasında “en iyi”nin ne demek olduğu argümanın her adaya hangi dönüşümle bağlandığına bakar. Dönüşümler iyiden kötüye şöyle sıralanır: tam uyum, tamsayı ya da kayan nokta terfisi, standart dönüşüm, kullanıcı tanımlı dönüşüm ve en sonda üç noktalı parametre. Bir aday her argümanda diğerlerinden kötü değilse ve en az bir argümanda daha iyiyse en iyidir. İki aday birbirini yenemiyorsa çağrı belirsizdir ve derleme durur; bu hatayı düzyazıda anlatıyorum, çünkü derlenmeyen kod blok olarak yazılmaz.

// siralama.cpp — donusum siralamasi
#include <cstdio>

const char* h(int) { return "int"; }
const char* h(long) { return "long"; }
const char* h(double) { return "double"; }

const char* g(long) { return "long"; }
const char* g(...) { return "uc nokta"; }

int main() {
    short s = 1;
    std::printf("h('a')  %s\n", h('a'));
    std::printf("h(s)    %s\n", h(s));
    std::printf("h(1.5f) %s\n", h(1.5f));
    std::printf("h(2L)   %s\n", h(2L));
    std::printf("g(1)    %s\n", g(1));
    std::printf("g(1.5)  %s\n", g(1.5));
    return 0;
}
h('a')  int
h(s)    int
h(1.5f) double
h(2L)   long
g(1)    long
g(1.5)  long

MP27. char ve short argümanları int’e terfiyle bağlanır ve terfi, long’a standart dönüşümden iyidir; bu yüzden ikisi de int adayını seçer. float, double’a terfiyle bağlanır. 2L için long tam uyumdur. g(1) için long’a standart dönüşüm ile üç nokta yarışır: dönüşüm kazanır. g(1.5) için de double’dan long’a standart dönüşüm vardır ve üç noktadan iyidir. Bu sıralamayı belirleyen standarttır; çıktı yalnız onu doğrular.

Öncelik Etiketi ile Aday Sıralamak

enable_if ile elenen adayların birden fazlası aynı anda uygunsa çağrı belirsizleşir. Öncelik kurmak için eski bir yöntem, etiket tiplerinden bir türetme zinciri kurmaktır. oncelik<2>, oncelik<1>’den, o da oncelik<0>’dan türer. Her aday son parametre olarak farklı bir etiket alır ve çağrı en yüksek etiketle yapılır: tam uyan en yüksek etikettir, elenenler listeden düşer, kalanlar arasından türetme mesafesi en kısa olan seçilir.

// oncelik.cpp — etiketle aday sirasi
#include <cstdio>
#include <string>
#include <type_traits>
#include <utility>

template <int N>
struct oncelik : oncelik<N - 1> {};
template <>
struct oncelik<0> {};

template <typename T>
auto ayir(const T& x, oncelik<2>) -> decltype(x.size(), "boyutlu") {
    return "boyutlu";
}

template <typename T, std::enable_if_t<std::is_pointer_v<T>, int> = 0>
const char* ayir(const T&, oncelik<1>) { return "isaretci"; }

template <typename T>
const char* ayir(const T&, oncelik<0>) { return "genel"; }

template <typename T>
const char* ayir(const T& x) { return ayir(x, oncelik<2>{}); }

int main() {
    int n = 3;
    std::printf("string  %s\n", ayir(std::string("ab")));
    std::printf("isaretci %s\n", ayir(&n));
    std::printf("int     %s\n", ayir(n));
    return 0;
}
string  boyutlu
isaretci isaretci
int     genel

MP28. std::string için iki aday uygundur, çünkü işaretçi adayı enable_if ile elenir; oncelik<2> en yüksek etikettir ve bir türetme adımı gerektirmez, boyutlu kazanır. int* için size() sorgusu bildirimde başarısız olur, ilk aday elenir ve oncelik<1> adayı seçilir. int için ikisi de elenir ve genel kalır. C++17 if constexpr ve C++20 kavramları bu zincirin yerini alır; ama etiket yöntemi eski kodda hâlâ yaygındır, o yüzden okuyabilmek gerekir.

void_t ile Algılama

enable_if bir koşulu açıkça yazar. Bazen soru, bir ifadenin tip için geçerli olup olmadığıdır. std::void_t<...> bu soruyu SFINAE’ye bağlar: argümanları ne olursa olsun void olur, ama argümanlardan biri geçersizse yerine koyma başarısız olur. Genel şablon false_type’tan türer; kısmi özelleştirme void_t içinde ifadeyi dener.

// bolunur.cpp — void_t ile algilama
#include <cstdio>
#include <string>
#include <type_traits>
#include <utility>

template <typename T, typename = void>
struct bolunur : std::false_type {};

template <typename T>
struct bolunur<T, std::void_t<decltype(std::declval<T>() / 2)>> : std::true_type {};

int main() {
    std::printf("bolunur<int>         %s\n", bolunur<int>::value ? "evet" : "hayir");
    std::printf("bolunur<double>      %s\n", bolunur<double>::value ? "evet" : "hayir");
    std::printf("bolunur<std::string> %s\n", bolunur<std::string>::value ? "evet" : "hayir");
    return 0;
}
bolunur<int>         evet
bolunur<double>      evet
bolunur<std::string> hayir

std::declval<T>() / 2 ifadesi int için geçerlidir, std::string için geçersizdir. Geçersizse kısmi özelleştirme elenir ve genel şablon kalır. Bu, ders başında sorulan soruya cevaptır: uygun olmayan aday hata vermez, aday listesinden düşer.

C++20: Kavram ve requires

enable_if ve void_t işi görür ama okunması zordur. C++20, aynı kısıtı doğrudan yazmak için kavram ve requires ifadesi getirdi. Bir kısıt sağlanmıyorsa şablon aday olmaz, derleyiciler de hata iletisinde genellikle uzun bir yerine koyma dökümü yerine sağlanmayan kısıtı gösterir.

// kavram.cpp — requires ile kisit
#include <cstdio>
#include <string>

template <typename T>
concept Bolunebilir = requires(T x) { x / 2; };

template <Bolunebilir T>
const char* yaris(T) { return "bolunur"; }

const char* yaris(...) { return "bolunmez"; }

int main() {
    std::printf("yaris(8)   %s\n", yaris(8));
    std::printf("yaris(str) %s\n", yaris(std::string("x")));
    return 0;
}
yaris(8)   bolunur
yaris(str) bolunmez

Kısıtlı şablon int için uygun adaydır ve üç noktalı aday daha az iyi olduğu için kazanır. string için kısıt sağlanmaz, şablon elenir ve geriye üç noktalı aday kalır. Kavram bir sorguyu adlandırır; seçim yine derlemede biter.

Elenen Adayın Gövdesi Yazılmaz

SFINAE’nin bedelinin neresinde olduğunu sayıyla görelim. Bir aday elenirse, gövdesi hiçbir zaman somutlaştırılmaz. Aşağıda iki adayın gövdesi yerel bir static ile kayıt altına alınır; çağrılmayan gövde kayıt bırakmaz.

// govde-sayisi.cpp — elenen adayin govdesi
#include <cstdio>
#include <type_traits>

inline int tamsayi_govde = 0;
inline int kayan_govde = 0;

template <typename T, std::enable_if_t<std::is_integral_v<T>, int> = 0>
int ikiye(T x) {
    static const bool kayit = (++tamsayi_govde, true);
    (void)kayit;
    return static_cast<int>(x) * 2;
}

template <typename T, std::enable_if_t<std::is_floating_point_v<T>, int> = 0>
int ikiye(T x) {
    static const bool kayit = (++kayan_govde, true);
    (void)kayit;
    return static_cast<int>(x * 2);
}

int main() {
    ikiye(7);
    ikiye(8);
    ikiye(9L);
    std::printf("tamsayi=%d kayan=%d\n", tamsayi_govde, kayan_govde);
    ikiye(2.5);
    std::printf("tamsayi=%d kayan=%d\n", tamsayi_govde, kayan_govde);
    return 0;
}
tamsayi=2 kayan=0
tamsayi=2 kayan=1

MP29. Üç tamsayı çağrısından sonra yalnız tamsayı gövdeleri vardır: int için bir, long için bir, yani iki gövde; kayan gövde sıfırdır. Sonra bir double çağrısı kayan gövdeyi bir yapar. Elenen aday listeden düşmüştür ve gövdesi hiç yazılmamıştır; bu yüzden SFINAE’nin bedeli yalnız elemenin kendisidir. Sayılar bir ürün sayısıdır, bir süre değil.

Kavramlarda Sıralama: Kapsama

Kavramlarla çalışan C++20 çözümlemesi, aynı adayın iki kısıtlı sürümü uygunsa, daha çok kısıtlananı seçer. Bir kavram bir başkasını içeriyorsa (std::signed_integral, std::integral’ı kapsar) kapsayan kavram daha özeldir. Bu kural, etiket zincirinin yerini alır: sıra, kavramların kendi yapısından çıkar.

// kisit-sira.cpp — kavram kapsamasi
#include <concepts>
#include <cstdio>

template <typename T>
const char* sinif(T) { return "genel"; }

template <std::integral T>
const char* sinif(T) { return "tamsayi"; }

template <std::signed_integral T>
const char* sinif(T) { return "isaretli"; }

int main() {
    std::printf("sinif(1)   %s\n", sinif(1));
    std::printf("sinif(1u)  %s\n", sinif(1u));
    std::printf("sinif(1.5) %s\n", sinif(1.5));
    return 0;
}
sinif(1)   isaretli
sinif(1u)  tamsayi
sinif(1.5) genel

MP30. int hem integral hem signed_integral’dır; daha çok kısıtlı olan isaretli seçilir. unsigned yalnız integral’dır, tamsayi kalır. double hiçbirini sağlamaz, genel kalır. Bu seçim yine derlemededir ve çağrı noktasında bir sorgu bırakmaz. Bedel derleme sınıfındadır; yalnız adayların toplanması ve kısıtların denetlenmesi derleyicinin işine eklenir.

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

SFINAE yalnız bildirimin hemen bağlamındaki yerine koyma hatalarını kapsar: dönüş tipi, parametre tipleri ve şablon parametreleri yazılırken doğrudan çıkan hatalar. Bu sırada başka bir şablonun somutlaştırılması gerekir ve hata onun içinde çıkarsa, eleme olmaz, derleme durur. İşlevin gövdesi aday seçiminden sonra somutlaştırılır. Gövdedeki bir hata artık aday listesinden eleme yapmaz, derlemeyi durdurur. Örneğin x.size() çağrısını gövdede yazan bir şablon int ile çağrılırsa, derleyici o şablonu uygun aday sayar, seçer, gövdeyi somutlaştırırken hata verir ve derleme durur.

Aynı çağrı bildirime taşınınca elemeye dönüşür. Aşağıdaki sürüm, size() sorgusunu dönüş tipinde yapar.

// bildirim.cpp — sorgu bildirimde, gövde hatasiz
#include <cstdio>
#include <string>

template <typename T>
auto boyut(const T& x) -> decltype(x.size(), long()) {
    return static_cast<long>(x.size());
}

long boyut(...) { return -1; }

int main() {
    std::printf("string %ld\n", boyut(std::string("abcd")));
    std::printf("int    %ld\n", boyut(7));
    return 0;
}
string 4
int    -1

MP31. string için dönüş tipi geçerlidir ve şablon seçilir. int için x.size() geçersizdir, yerine koyma başarısız olur ve üç noktalı aday kalır. Aynı sorgu gövdede olsaydı derleme dururdu. Bu yüzden sayı eleme olayının kanıtıdır; ölçü sorusunun yanıtı bu elemenin yapıldığı aşamadır: derleme.

Özet

  • Bu dersin ölçtüğü düzenek bedeli derleme sınıfına taşır: aday toplama, uygun adayı eleme ve en iyi adayı seçme derlemede biter, koşumda sorgu kalmaz.
  • Aday, uygun aday ve en iyi aday sırasıyla toplanır, elenir ve seçilir; eşitlikte şablon olmayan kazanır, tam uyum dönüşümü yener.
  • SFINAE’de yerine koyma başarısızlığı hata değildir: enable_if_t ile tur(7) tamsayi, tur(2.5) kayan seçilir.
  • void_t bir ifadenin geçerliliğini algılar: bolunur<int> evet, bolunur<std::string> hayir.
  • C++20 concept ve requires aynı kısıtı okunur yazar; SFINAE yalnız bildirimi kapsar, gövdedeki hata derlemeyi durdurur.

Sonraki Adım

Bu derste aday elemesi derlemede bitti ve koşumda hiçbir sorgu bırakmadı. Sıradaki ders bu fikri bir sınıf hiyerarşisine uyguluyor: sanal işlev yerine şablon kullanılırsa gönderim derlemeye taşınır, ama her tip için ayrı gövde yazdırılır. Bu değiş tokuşun ölçüsü ne?

İ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