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_tiletur(7)tamsayi,tur(2.5)kayan seçilir. void_tbir ifadenin geçerliliğini algılar:bolunur<int>evet,bolunur<std::string>hayir.- C++20
conceptverequiresaynı 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.