Ders 04 / 12
Tip Özellikleri
Standart tip özellikleri, if constexpr ile derleme zamanında seçim, static_assert, kendi tip özelliğini yazmak ve seçimin koşumda sıfır sorgu bırakması.
Yayın Güncelleme
Bu derste
- Standart tip özelliklerini (is_integral_v, is_same_v, remove_cvref_t, conditional_t) sorgu ve dönüşüm olarak ayırt eder.
- if constexpr ile seçimin atılan dalı o somutlaştırmada derlemediğini gösterir.
- Kendi tip özelliğini true_type ve false_type üzerine kurar.
- Derleme zamanında verilen kararın koşumda sıfır sorgu bıraktığını, constexpr'in ise bunu tek başına garanti etmediğini açıklar.
İçindekiler
Önceki ders paket açmanın kaç gövde yazdırdığını saydı. Şablonun içinde tipe bakıp davranışı değiştirmek gerektiğinde ikinci bir soru çıkar: bu bakış ne zaman yapılır ve kim öder? Tip özelliği, bir tip hakkında derleme sırasında yanıt veren küçük bir şablondur. Dersin iddiası şu: seçim derlemede verildiğinde koşumda sorgu kalmaz. Bedel derleme sınıfında kalır ve yanında ürün sayısını taşır.
Bu ayrım M11/K01’den farklı bir soruya cevap verir. K01 kararı kimin verdiğini sordu; bu
kurs ise bedelin kimin ödediğini ve neyle sayıldığını soruyor. K01’in derleyici sınıfı bir
kararı adlandırırdı. Buradaki derleme sınıfı bir bedeli adlandırır ve yanında daima
bir ürün sayısı taşır.
Sorgular ve Dönüşümler
<type_traits> iki tür özellik verir. Sorgular bir bool üretir: std::is_integral_v<T>
tip bir tamsayı tipiyse doğrudur, std::is_same_v<A, B> iki tip özdeşse doğrudur.
Dönüşümler yeni bir tip üretir: std::remove_cvref_t<T> bir tipten const, volatile
ve referans niteleyicilerini soyar; std::conditional_t<K, A, B> koşul doğruysa A,
değilse B olur. Sonekli _v ve _t biçimleri, ::value ve ::type yazımının kısa
halidir. Hepsi derlemede değerlendirilir.
// ozellikler.cpp — sorgular ve donusumler
#include <cstdio>
#include <string>
#include <type_traits>
int main() {
std::printf("integral %d %d %d\n", std::is_integral_v<int>,
std::is_integral_v<double>, std::is_integral_v<bool>);
std::printf("ayni %d %d\n", std::is_same_v<int, int>,
std::is_same_v<int, long>);
std::printf("soyma %d %d\n",
std::is_same_v<std::remove_cvref_t<const int&>, int>,
std::is_same_v<std::remove_cvref_t<std::string&&>, std::string>);
using secilen = std::conditional_t<(sizeof(int) >= 2), int, long>;
std::printf("kosul %d\n", std::is_same_v<secilen, int>);
return 0;
}
integral 1 0 1 ayni 1 0 soyma 1 1 kosul 1
bool bir tamsayı tipidir, bu yüzden üçüncü sorgu doğrudur. const int& soyulunca int
olur; referans niteleyicisi (&&) de soyulur. conditional_t bir değer değil bir tip seçer:
secilen burada int’e eşittir. Bu çıktı standardın garantisidir; ilk iki sorgunun
sonucu hiçbir uygulamada değişmez.
if constexpr ile Seçim
Sorgu sonucunu bir dal seçmek için kullanmanın yolu if constexpr’dır. Koşul derlemede
değerlendirilir ve atılan dal o somutlaştırmada derlenmez. Sıradan if ise iki dalı da
derler; bir dalda x.size() yazıp T olarak int verirseniz, o dal hiç çalışmasa bile
derleme durur. if constexpr bu sorunu çözer: T tamsayıysa .size() dalına derleyici
bakmaz.
Aşağıda tur her tipe bir etiket verir. tur_etiketli aynı işi bir koşum parametresiyle,
yani çalışırken verilen kararla yapar ve her çağrıda sorgu sayar.
// tur.hpp — derleme zamani ve kosum zamani secim
#ifndef TUR_HPP
#define TUR_HPP
#include <string>
#include <type_traits>
inline int sorgu = 0;
inline int etiketli_sorgu = 0;
template <typename T>
const char* tur(T) {
if constexpr (std::is_integral_v<T>) {
return "tamsayi";
} else if constexpr (std::is_floating_point_v<T>) {
return "kayan";
} else {
return "diger";
}
}
template <typename T>
const char* tur_etiketli(T, int etiket) {
++etiketli_sorgu;
if (etiket == 0) {
return "tamsayi";
}
if (etiket == 1) {
return "kayan";
}
return "diger";
}
template <typename T>
long uzunluk(T x) {
if constexpr (std::is_same_v<std::remove_cvref_t<T>, std::string>) {
return static_cast<long>(x.size());
} else {
return static_cast<long>(x);
}
}
#endif
// tur-sec.cpp — ayni sonuc, farkli sorgu sayisi
#include <cstdio>
#include <string>
#include "tur.hpp"
int main() {
int ayni = 0;
ayni += std::string(tur(1)) == tur_etiketli(1, 0);
ayni += std::string(tur(2L)) == tur_etiketli(2L, 0);
ayni += std::string(tur('a')) == tur_etiketli('a', 0);
ayni += std::string(tur(1.5)) == tur_etiketli(1.5, 1);
ayni += std::string(tur(2.5f)) == tur_etiketli(2.5f, 1);
ayni += std::string(tur(std::string("x"))) == tur_etiketli(std::string("x"), 2);
std::printf("ayni=%d sorgu=%d etiketli=%d\n", ayni, sorgu, etiketli_sorgu);
std::printf("uzunluk %ld %ld\n", uzunluk(std::string("abcd")), uzunluk(7));
return 0;
}
ayni=6 sorgu=0 etiketli=6 uzunluk 4 7
MP20. Altı çağrının hepsinde iki yöntem aynı etiketi verir (ayni=6). tur koşumda
hiçbir sorgu yapmaz (sorgu=0): hangi dalın seçileceği derleyicide karar verilmiştir ve
her somutlaştırmada tek bir return kalır. tur_etiketli ise her çağrıda bir etiket
okur (etiketli=6). Bu satır, çekirdekteki if constexpr ile secim ölçümüdür. uzunluk
da atılan dalın neden derlenmediğini gösterir: int için x.size() dalı yoktur, string
için static_cast<long>(x) dalı yoktur. Düz if ile ikisi de derleme hatası olurdu.
static_assert ve Kısıt
static_assert(koşul, "mesaj") koşulu derlemede denetler. Koşul yanlışsa derleme durur ve
mesaj derleyici çıktısında görünür; doğruysa kod üretilmez. Burada başarısızlık düzyazıda
anlatılır, çünkü derlenmeyen kod blok olarak yazılmaz: static_assert(std::is_integral_v<T>)
içeren bir şablonu double ile somutlaştırmak derlemeyi durdurur; derleyiciler genellikle
mesajın yanında somutlaştırmanın nereden istendiğini de gösterir. Böylece yanlış kullanım, koşumdaki bir hata yerine derlemede yakalanır.
Kendi Tip Özelliğinizi Yazmak
Standart özellikler std::true_type ve std::false_type üzerine kuruludur. İkisinin de
::value üyesi vardır. Kendi özelliğinizi yazmak için genel şablon false_type’tan
türer, ilgilendiğiniz tip için kısmi özelleştirme true_type’tan türer.
// ozel-ozellik.cpp — kendi tip ozelligi
#include <cstdio>
#include <string>
#include <type_traits>
#include <vector>
template <typename T>
struct dizi_mi : std::false_type {};
template <typename U>
struct dizi_mi<std::vector<U>> : std::true_type {};
template <typename T>
inline constexpr bool dizi_mi_v = dizi_mi<T>::value;
static_assert(dizi_mi_v<std::vector<int>>, "vector dizidir");
static_assert(!dizi_mi_v<int>, "int dizi degildir");
int main() {
std::printf("dizi_mi %d %d %d\n", dizi_mi_v<std::vector<double>>,
dizi_mi_v<std::string>, dizi_mi_v<int>);
return 0;
}
dizi_mi 1 0 0
Şablon Özelleştirme dersindeki kısmi özelleştirme burada işe yarıyor: std::vector<U> kalıbı eşleşirse
true_type seçilir, hiçbiri eşleşmezse genel şablon kalır. O derste görüldüğü gibi
özelleştirmenin ilk kullanımdan önce görünür olması gerekir. İki static_assert bu
dosyada hiçbir kod üretmez; program derlenebildiyse ikisi de geçmiştir.
Tipler Arası İlişkiler
Bazı sorgular tek bir tipe değil, iki tip arasındaki ilişkiye bakar. std::is_base_of_v<B, D>
D, B’den türemişse doğrudur. std::is_convertible_v<A, B> bir A değerinin örtük olarak
B’ye dönüşüp dönüşemeyeceğini sorar. std::is_trivially_copyable_v<T> tipin bellek
kopyasıyla kopyalanabilir olup olmadığını söyler; bir std::string içinde sahiplik taşıdığı
için bu soruda false verir: kopyası bayt bayt değil, kendi kopya yapıcısıyla yapılır. std::decay_t<T> ise bir değerin işleve değer olarak
verilmesindeki tip dönüşümünü yapar: referansı ve const’u soyar, dizi
tipini işaretçiye dönüştürür.
// iliski.cpp — tip iliskileri ve decay
#include <cstdio>
#include <string>
#include <type_traits>
struct Taban {};
struct Tureyen : Taban {};
int main() {
std::printf("taban %d %d\n", std::is_base_of_v<Taban, Tureyen>,
std::is_base_of_v<Tureyen, Taban>);
std::printf("donusum %d %d\n", std::is_convertible_v<int, double>,
std::is_convertible_v<std::string, int>);
std::printf("kopya %d %d\n", std::is_trivially_copyable_v<int>,
std::is_trivially_copyable_v<std::string>);
std::printf("decay %d %d\n",
std::is_same_v<std::decay_t<const int&>, int>,
std::is_same_v<std::decay_t<int[3]>, int*>);
return 0;
}
taban 1 0 donusum 1 0 kopya 1 0 decay 1 1
MP21. is_base_of yönlüdür: Tureyen, Taban’dan türer, tersi doğru değildir.
int, double’a dönüşür; std::string, int’e dönüşmez. int bellek kopyasıyla
kopyalanabilir, std::string ise bellek kopyasıyla kopyalanamaz. decay_t<int[3]> dizi tipini int*’e
indirger. Bu sorguların hepsi derlemede cevaplanır; çıktıdaki satırlar derlemenin
ürettiği cevaplardır, bir süre değil. Hiçbiri koşumda bir sorgu bırakmaz: int ve std::string
için değerler zaten derleyicinin elindedir.
Kendi Dönüşümünüzü Yazmak
Dönüşümler de aynı kalıpla yazılır. Genel şablon tipi olduğu gibi bırakır, kısmi
özelleştirmeler bir kalıbı soyar. Standart std::remove_pointer_t bir işaretçi katmanını
soyar; burada bütün katmanları soyan özyineli bir sürümü bir _t takma adıyla birlikte
kuruyoruz. using ile tanımlanan ::tip üyesi,
standart kütüphanenin ::type’ının yerini tutar.
// soy.cpp — kendi donusum ozelligi
#include <cstdio>
#include <type_traits>
template <typename T>
struct soy_isaretci {
using tip = T;
};
template <typename T>
struct soy_isaretci<T*> {
using tip = typename soy_isaretci<T>::tip;
};
template <typename T>
using soy_isaretci_t = typename soy_isaretci<T>::tip;
static_assert(std::is_same_v<soy_isaretci_t<int>, int>);
static_assert(std::is_same_v<soy_isaretci_t<int*>, int>);
int main() {
std::printf("soy %d %d %d\n", std::is_same_v<soy_isaretci_t<char**>, char>,
std::is_same_v<soy_isaretci_t<const int*>, const int>,
std::is_same_v<soy_isaretci_t<double>, double>);
return 0;
}
soy 1 1 1
MP22. Kısmi özelleştirme bir işaretçi katmanını soyar ve kendi kendini çağırarak kalan
tipe iner: char** iki adımda char olur. Bu özyineleme derleyicinin içindedir; her
katman için bir somutlaştırma yazdırır ve koşumda iz bırakmaz. Dikkat edilmesi gereken
bir nokta const int* örneğidir: işaretçinin kendisi değil gösterdiği tip const int’tir,
bu yüzden sonuç const int olarak kalır. İşaretçinin kendisinin const’luğu başka bir
konumdadır ve bu kalıp ona bakmaz.
Tip Özelliği ile Kısıt Birleştirmek
Tip özellikleri tek tek değil, birlikte de sorulabilir. Bir
işlev yalnız işaretsiz bir tamsayı tipi için çalışsın istersek static_assert ile birkaç
özelliği birlikte denetleriz: std::is_integral_v<T> && std::is_unsigned_v<T>. Mantık
işleçleriyle birleştirilebilen koşullar std::conjunction_v, std::disjunction_v ve
std::negation_v ile de yazılabilir; bu ikinci yazım kısa devre yapar, yani sonraki
özellikleri koşul belli olunca somutlaştırmaz.
// birlestir.cpp — ozellikleri birlestirmek
#include <cstdio>
#include <type_traits>
template <typename T>
constexpr bool isaretsiz_tamsayi_v = std::is_integral_v<T> && std::is_unsigned_v<T>;
template <typename T>
constexpr bool sayisal_degil_v =
std::conjunction_v<std::negation<std::is_integral<T>>,
std::negation<std::is_floating_point<T>>>;
int main() {
std::printf("isaretsiz %d %d %d\n", isaretsiz_tamsayi_v<unsigned>,
isaretsiz_tamsayi_v<int>, isaretsiz_tamsayi_v<double>);
std::printf("sayisal_degil %d %d\n", sayisal_degil_v<int>,
sayisal_degil_v<const char*>);
return 0;
}
isaretsiz 1 0 0 sayisal_degil 0 1
MP23. unsigned hem tamsayı hem işaretsizdir, int işaretlidir, double tamsayı
değildir. sayisal_degil_v yalnız tamsayı ve kayan nokta olmayan tiplerde doğrudur: int
için yanlış, işaretçi için doğru. Burada kayıt altına alınacak şey, bu bileşimlerin de
derleyicide tamamen çözülmesidir; koşumda iz kalmaz, bedel yine derleme sınıfındadır.
Dönüş Tipi de Seçilir
if constexpr dalları farklı tipte değer döndürebilir; atılan dal dönüş tipi çıkarımına
katılmaz. auto dönüş tipi bu yüzden her somutlaştırmada tek bir return görür. Sıradan
bir if ile aynı işlevde long ve double döndürmek, iki return farklı tip
çıkardığı için derleme hatasıdır. Dönüş tipini açıkça yazmak istersek std::conditional_t
bir tip adı hesaplar.
// donus.cpp — dallara gore donus tipi
#include <cstdio>
#include <type_traits>
template <typename T>
auto ikiyle(T x) {
if constexpr (std::is_integral_v<T>) {
return x * 2L;
} else {
return x * 2.0;
}
}
template <typename T>
using genis_t = std::conditional_t<std::is_integral_v<T>, long long, double>;
int main() {
std::printf("ikiyle %d %d\n",
std::is_same_v<decltype(ikiyle(1)), long>,
std::is_same_v<decltype(ikiyle(1.5f)), double>);
std::printf("genis %d %d\n", std::is_same_v<genis_t<short>, long long>,
std::is_same_v<genis_t<float>, double>);
std::printf("deger %ld %.1f\n", ikiyle(21), ikiyle(1.25f));
return 0;
}
ikiyle 1 1 genis 1 1 deger 42 2.5
MP24. ikiyle(1) tamsayı dalını seçer ve long döndürür; ikiyle(1.5f) diğer dalı seçer
ve double döndürür. İki dönüş tipi tek işlevin iki somutlaştırmasındadır, ayrı gövdeleri
vardır. genis_t aynı kararı bir tip takma adına taşır. İkinci satırdaki short ve
float argümanları, tamsayı ve kayan nokta olarak ayrı dallara düşer. Seçim derlemede
bittiği için koşumda dal sorgusu bırakmaz.
Desteklenmeyen tipleri bir else dalında static_assert ile yasaklamak istiyorsanız,
koşulun T’ye bağımlı olması gerekir; bağımlı olmayan bir koşul, o dal hiç seçilmese bile
şablon tanımlanırken değerlendirilebilir ve derlemeyi durdurabilir. Bu yüzden bu tür
dallarda koşulu T’ye bağlayan bir tip özelliği kullanın.
Sınırlayıcı Ölçüm
Derleme zamanında verilen karar koşumda bir adım bırakmaz, ama constexpr yazmak bunu
tek başına garanti etmez. constexpr bir işlev, hem derlemede hem koşumda çağrılabilir
demektir. Hangisinin olacağını bağlam belirler: bir constexpr değişkenin ilk değeri ya da
static_assert gibi sabit değerlendirme gerektiren bir bağlam değerlendirmeyi derlemeye
taşır. std::is_constant_evaluated() bu iki durumu ayırt eder.
// sabit-degerlendirme.cpp — ayni constexpr islev, iki baglam
#include <cstdio>
#include <type_traits>
inline int adim = 0;
constexpr int kareler_toplami(int n) {
int t = 0;
for (int i = 1; i <= n; ++i) {
if (!std::is_constant_evaluated()) {
++adim;
}
t += i * i;
}
return t;
}
int main() {
constexpr int a = kareler_toplami(10);
std::printf("derleme sonuc=%d adim=%d\n", a, adim);
volatile int n = 10;
int b = kareler_toplami(n);
std::printf("kosum sonuc=%d adim=%d\n", b, adim);
return 0;
}
derleme sonuc=385 adim=0 kosum sonuc=385 adim=10
MP25. Aynı işlev, aynı sonucu verir; constexpr değişkene atanan çağrı derlemede
değerlendirilir ve sayaca dokunmaz (adim=0). volatile bir argüman ise derleyicinin
değeri bilmesini engeller; çağrı koşumda yapılır ve on yineleme sayılır (adim=10). Bu
yüzden constexpr bir söz değil, bir izindir: bedeli derlemeye taşıyan, sabit değerlendirme
gerektiren bağlamdır. Sayı sınıflandırmanın kanıtıdır; ölçü sorusunun yanıtı bir sınıf
adıdır, ve burada bu ad derleme sınıfıdır.
Özet
- Bu dersin ölçtüğü düzenek bedeli derleme sınıfına taşır: tip sorgusu ve dalın
seçilmesi derlemede biter, koşumda
sorgu=0kalır. - Sorgular (
is_integral_v,is_same_v)boolverir; dönüşümler (remove_cvref_t,conditional_t) tip verir. if constexpratılan dalı o somutlaştırmada derlemez; düzifiki dalı da derler.- Kendi tip özelliği genel şablonun
false_type’a, kısmi özelleştirmenintrue_type’a türemesiyle kurulur;static_assertyanlış kullanımı derlemede durdurur. constexprtaşımayı garanti etmez: aynı işlev sabit değerlendirmedeadim=0, koşumdaadim=10verir.
Sonraki Adım
Bu derste tip sorgusu, seçimi derlemeye çekti ve koşumda sıfır sorgu bıraktı. Sıradaki ders, aynı sorguyu bir adım geriye alıyor: bir aday şablon uygun değilse derleyici onu hata vermeden nasıl eler, ve aşırı yükleme çözümlemesi bu elemeden sonra neyi seçer?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.