İçeriğe geç
academia.sh

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=0 kalır.
  • Sorgular (is_integral_v, is_same_v) bool verir; dönüşümler (remove_cvref_t, conditional_t) tip verir.
  • if constexpr atılan dalı o somutlaştırmada derlemez; düz if iki dalı da derler.
  • Kendi tip özelliği genel şablonun false_type’a, kısmi özelleştirmenin true_type’a türemesiyle kurulur; static_assert yanlış kullanımı derlemede durdurur.
  • constexpr taşımayı garanti etmez: aynı işlev sabit değerlendirmede adim=0, koşumda adim=10 verir.

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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat