D Programlama Dili – Programlama dersleri ve D referansı
Ali Çehreli

arayüz: [interface], yapının, sınıfın, veya modülün sunduğu işlevler
birlik: [union], birden fazla değişkeni aynı bellek bölgesinde depolayan veri yapısı
blok: [block], küme parantezleriyle gruplanmış ifadelerin tümü
CTFE: [Compile Time Function Execution], derleme zamanında işlev işletme
çokuzlu: [tuple], bir kaç parçanın diziye benzer biçimde bir araya gelmesinden oluşan yapı
hazır değer: [literal], kod içinde hazır olarak yazılan değer
isim alanı: [name scope], ismin geçerli olduğu kapsam
kapsam: [scope], küme parantezleriyle belirlenen bir alan
kısıtlama: [constraint], şablon parametrelerinin uyması gereken koşulların belirlenmesi
ördek tipleme: [duck typing], türün değil, davranışın önemli olması
özelleme: [specialization], şablonun bir özel tanımı
özyineleme: [recursion], bir işlevin doğrudan veya dolaylı olarak kendisini çağırması
sıradüzen: [hierarchy], sınıfların türeyerek oluşturdukları aile ağacı
şablon: [template], derleyicinin örneğin 'türden bağımsız programlama' için kod üretme düzeneği
... bütün sözlük



İngilizce Kaynaklar


Diğer




Ayrıntılı Şablonlar

Şablonların ne kadar kullanışlı olduklarını Şablonlar bölümünde görmüştük. Algoritmaların veya veri yapılarının tek tanımını yazarak birden çok türle çalışmalarını sağlayabiliyorduk.

O bölümde şablonların en çok karşılaşılan kullanımlarını göstermiştim. Bu bölümde şablon olanağını daha ayrıntılı olarak göreceğiz. Devam etmeden önce en azından o bölümün sonundaki özeti bir kere daha gözden geçirmenizi öneririm; o bölümde anlatılanları burada tekrarlamamaya çalışacağım.

Daha önce işlev, yapı, ve sınıf şablonlarını tanımıştık ve şablon parametrelerinin türler konusunda serbestlik getirdiklerini görmüştük. Bu bölümde; hem birlik ve arayüz şablonlarını da tanıyacağız; hem de şablon parametrelerinin değer, this, alias, ve çokuzlu çeşitleri olduğunu da göreceğiz.

Kestirme ve uzun söz dizimi

C++ gibi başka dillerde de olduğu gibi D'nin şablonları çok güçlü olanaklardır. Buna rağmen, en çok yararlanılan kullanımlarının olabildiğince rahat ve anlaşılır olmasına çalışılmıştır. İşlev, yapı, veya sınıf şablonu tanımlamak; isminden sonra şablon parametre listesi eklemek kadar kolaydır:

T ikiKatı(T)(T değer) {
    return 2 * değer;
}

class Kesirli(T) {
    T pay;
    T payda;

    // ...
}

Daha önce de görmüş olduğunuz yukarıdaki tanımlar, D'nin kestirme şablon tanımlarıdır.

Aslında şablonlar daha uzun olarak template anahtar sözcüğü ile tanımlanırlar. Yukarıdaki söz dizimleri, aşağıdaki tanımların kısa eşdeğerleridir:

template ikiKatı(T) {
    T ikiKatı(T değer) {
        return 2 * değer;
    }
}

template Kesirli(T) {
    class Kesirli {
        T pay;
        T payda;

        // ...
    }
}

Derleyicinin her zaman için uzun tanımı kullandığını, ve kestirme söz dizimini arka planda şu şekilde uzun tanıma dönüştürdüğünü düşünebiliriz:

  1. Tanımladığımız şablonu bir template kapsamı içine alır.
  2. O kapsama da aynı ismi verir.
  3. Şablon parametre listesini bizim tanımladığımız şablondan alır ve o kapsama verir.

Kestirme tanım; biraz aşağıda göreceğimiz tek tanım içeren şablon olanağı ile ilgilidir.

Şablon isim alanı

template bloğu, aslında bir seferde birden çok şablon tanımlanmasına da olanak verir:

template ŞablonBloğu(T) {
    T birİşlev(T değer) {
        return değer / 3;
    }

    struct BirYapı {
        T üye;
    }
}

Yukarıdaki blokta bir işlev bir de yapı şablonu tanımlamaktadır. O şablonları örneğin int ve double türleri için, ve uzun isimleriyle şöyle kullanabiliriz:

    auto sonuç = ŞablonBloğu!int.birİşlev(42);
    writeln(sonuç);

    auto nesne = ŞablonBloğu!double.BirYapı(5.6);
    writeln(nesne.üye);

Şablonun belirli bir türle kullanımı bir isim alanı tanımlar. Bloğun içindeki isimler o isim alanı açıkça belirtilerek kullanılabilirler. Bu isimler fazla uzun olabileceklerinden onlara alias bölümünde gördüğümüz alias anahtar sözcüğü ile kısa takma isimler verilebilir:

    alias KarakterYapısı = ŞablonBloğu!dchar.BirYapı;

// ...

    auto nesne = KarakterYapısı('ğ');
    writeln(nesne.üye);
Aynı isimde tanım içeren template blokları

Şablon bloğunun ismi ile aynı isimde tanım içeren şablon blokları içerdikleri o tanımın yerine geçerler. Bu, şimdiye kadarki şablonlarda kullandığımız kestirme söz dizimini sağlayan olanaktır. (Not: Bu olanağa İngilizce'de eponymous templates denir.)

Örnek olarak, büyüklüğü 20 bayttan fazla olan türlerin büyük olarak kabul edildiği bir program olsun. Bir türün büyük olup olmadığının kararı şöyle bir şablonun içindeki bir bool değişken ile belirlenebilir:

template büyük_mü(T) {
    enum büyük_mü = T.sizeof > 20;
}

Dikkat ederseniz, hem şablonun hem de içindeki tanımın isimleri aynıdır. Öyle olduğunda bu uzun şablon tanımının isim alanı ve içindeki tanım açıkça büyük_mü!int.büyük_mü diye yazılmaz, kısaca yalnızca şablonun isim alanı yazılır:

    writeln(büyük_mü!int);

Yukarıdaki işaretli bölüm, şablon içindeki aynı isimli bool yerine geçer. Yukarıdaki kod çıktıya false yazar çünkü büyük_mü!int, şablon içindeki bool türündeki değişkendir ve int'in uzunluğu 4 bayt olduğundan o bool değişkenin değeri false'tur.

Yukarıdaki aynı isimde tanım içeren şablon, kısa söz dizimiyle de tanımlanabilir:

enum büyük_mü(T) = T.sizeof > 20;

Aynı isimde tanım içeren şablonların yaygın bir kullanımı, türlere takma isimler vermektir. Örneğin, aşağıdaki şablon verilen türlerden büyük olanına eşdeğer olan bir alias tanımlamaktadır:

template Büyüğü(A, B) {
    static if (A.sizeof < B.sizeof) {
        alias Büyüğü = B;

    } else {
        alias Büyüğü = A;
    }
}

Sekiz bayttan oluşan long türü dört bayttan oluşan int türünden daha büyük olduğundan Büyüğü!(int, long), long'un eşdeğeri olur. Bu çeşit şablonlar A ve B gibi türlerin kendilerinin şablon parametreleri oldukları durumlarda özellikle yararlıdırlar:

// ...

/* Bu işlevin dönüş türü, şablon parametrelerinden büyük
 * olanıdır: Ya A ya da B. */
auto hesapla(A, B)(A a, B b) {
    Büyüğü!(A, B) sonuç;
    // ...
    return sonuç;
}

void main() {
    auto h = hesapla(1, 2L);
    static assert(is (typeof(h) == long));
}
Şablon çeşitleri
İşlev, sınıf, ve yapı şablonları

Bu alt başlığı bütünlük amacıyla yazdım.

Yukarıda da görüldüğü gibi, bu tür şablonlarla hem Şablonlar bölümünde hem de daha sonraki örneklerde çok karşılaştık.

Üye işlev şablonları

Yapı ve sınıf üye işlevleri de şablon olabilir. Örneğin, aşağıdaki ekle() üye işlev şablonu, içindeki işlemlerle uyumlu olduğu sürece her türden değişkeni kabul eder (bu örnekteki tek şart, o değişkenin to!string ile kullanılabilmesidir):

class Toplayıcı {
    string içerik;

    void ekle(T)(auto ref const T değer) {
        import std.conv;
        içerik ~= değer.to!string;
    }
}

Ancak, şablonların teoride sonsuz farklı kullanımı olabileceğinden, sanal işlev olamazlar çünkü derleyici şablonun hangi kullanımlarının sınıfın arayüzüne dahil edileceğine karar veremez. (Sanal işlev olamadıklarından abstract anahtar sözcüğü ile de tanımlanamazlar.)

Örneğin, aşağıdaki alt sınıfın ekle() şablonu üst sınıftaki aynı isimli işlevin yeni tanımını veriyormuş gibi görünse de aslında isim gizlemeye neden olur (isim gizlemeyi alias bölümünde görmüştük):

class Toplayıcı {
    string içerik;

    void ekle(T)(auto ref const T değer) {
        import std.conv;
        içerik ~= değer.to!string;
    }
}

class KümeParantezliToplayıcı : Toplayıcı {
    /* Bu şablon üst sınıftakinin yeni tanımı değildir; üst
     * sınıftaki 'ekle' ismini gizlemektedir. */
    void ekle(T)(auto ref const T değer) {
        import std.string;
        super.ekle(format("{%s}", değer));
    }
}

void toplayıcıyıDoldur(Toplayıcı toplayıcı) {
    /* Aşağıdaki işlev çağrıları sanal değildir. Buradaki
     * 'toplayıcı' parametresinin türü 'Toplayıcı' olduğundan
     * her iki çağrı da Toplayıcı.ekle şablonuna
     * devredilirler. */

    toplayıcı.ekle(42);
    toplayıcı.ekle("merhaba");
}

void main() {
    auto toplayıcı = new KümeParantezliToplayıcı();
    toplayıcıyıDoldur(toplayıcı);

    import std.stdio;
    writeln(toplayıcı.içerik);
}

Sonuçta, asıl nesnenin türü KümeParantezliToplayıcı olduğu halde, toplayıcıyıDoldur() işlevinin içindeki bütün çağrılar parametresinin türü olan Toplayıcı'ya sevk edilir. İçerik KümeParantezliToplayıcı.ekle() işlevinin yerleştirdiği küme parantezlerini içermemektedir:

42merhaba    ← KümeParantezliToplayıcı'nın işi değil
Birlik şablonları

Birlik şablonları, yapı şablonları ile aynı şekilde tanımlanırlar. Birlik şablonları için de kestirme şablon söz dizimi kullanılabilir.

Bir örnek olarak, Birlikler bölümünde tanımladığımız IpAdresi birliğinin daha genel ve daha kullanışlı olanını tasarlamaya çalışalım. O bölümdeki birlik; değer olarak uint türünü kullanıyordu. O değerin parçalarına erişmek için kullanılan dizinin elemanlarının türü de ubyte idi:

union IpAdresi {
    uint değer;
    ubyte[4] baytlar;
}

O birlik, hem IPv4 adresi değeri tutuyordu, hem de o değerin parçalarına ayrı ayrı erişme olanağı veriyordu.

Aynı kavramı daha genel isimler de kullanarak bir şablon halinde şöyle tanımlayabiliriz:

union ParçalıDeğer(AsılTür, ParçaTürü) {
    AsılTür değer;
    ParçaTürü[/* gereken eleman adedi */] parçalar;
}

Bu birlik şablonu, asıl değerin ve alt parçalarının türünü serbestçe tanımlama olanağı verir. Asıl tür ve parça türü, birbirlerinden bağımsız olarak seçilebilirler.

Burada gereken bir işlem, parça dizisinin uzunluğunun kullanılan türlere bağlı olarak hesaplanmasıdır. IpAdresi birliğinde, uint'in dört adet ubyte parçası olduğunu bildiğimiz için sabit olarak 4 yazabilmiştik. Bu şablonda ise dizinin uzunluğu, kullanılan türlere göre otomatik olarak hesaplanmalıdır.

Türlerin bayt olarak uzunluklarının .sizeof niteliğinden öğrenilebildiğini biliyoruz. Kaç parça gerektiği bilgisini .sizeof niteliğinden yararlanan ve kısa söz dizimine olanak veren bir şablon içinde hesaplayabiliriz:

template elemanAdedi(AsılTür, ParçaTürü) {
    enum elemanAdedi = (AsılTür.sizeof + (ParçaTürü.sizeof - 1))
                       / ParçaTürü.sizeof;
}

Not: O hesaptaki (ParçaTürü.sizeof - 1) ifadesi, türlerin uzunluklarının birbirlerine tam olarak bölünemediği durumlarda gerekir. Asıl türün 5 bayt, parça türünün 2 bayt olduğunu düşünün. Aslında 3 parça gerektiği halde o ifade eklenmediğinde 5/2 hesabının sonucu tamsayı kırpılması nedeniyle 2 çıkar.

Artık parça dizisinin eleman adedi olarak o şablonun değerini kullanabiliriz ve böylece birliğin tanımı tamamlanmış olur:

union ParçalıDeğer(AsılTür, ParçaTürü) {
    AsılTür değer;
    ParçaTürü[elemanAdedi!(AsılTür, ParçaTürü)] parçalar;
}

Daha önce tanımladığımız IpAdresi birliğinin eşdeğeri olarak bu şablonu kullanmak istesek, türleri IpAdresi'nde olduğu gibi sırasıyla uint ve ubyte olarak belirtmemiz gerekir:

import std.stdio;

void main() {
    auto adres = ParçalıDeğer!(uint, ubyte)(0xc0a80102);

    foreach (eleman; adres.parçalar) {
        write(eleman, ' ');
    }
}

Birlikler bölümünde gördüğümüz çıktının aynısını elde ederiz:

2 1 168 192

Bu şablonun getirdiği esnekliği görmek için IPv4 adresinin parçalarını iki adet ushort olarak edinmek istediğimizi düşünelim. Bu sefer, ParçalıDeğer şablonunun ParçaTürü parametresi olarak ushort yazmak yeterlidir:

    auto adres = ParçalıDeğer!(uint, ushort)(0xc0a80102);

Alışık olmadığımız bir düzende olsa da, bu seferki çıktı iki ushort'tan oluşmaktadır:

258 49320
Arayüz şablonları

Arayüz şablonları arayüzde kullanılan türler, değerler, vs. konusunda serbestlik getirirler. Arayüz şablonlarında da kestirme tanım kullanılabilir.

Örnek olarak, renkli nesnelerin arayüzünü tanımlayan ama renk olarak hangi türün kullanılacağını serbest bırakan bir arayüz tasarlayalım:

interface RenkliNesne(RenkTürü) {
    void renklendir(RenkTürü renk);
}

O arayüz, kendisinden türeyen sınıfların renklendir işlevini tanımlamalarını gerektirir; ama renk olarak ne tür kullanılacağı konusunu serbest bırakır.

Bir sitedeki bir çerçeveyi temsil eden bir sınıf; renk olarak kırmızı, yeşil, ve maviden oluşan üçlü bir yapı kullanabilir:

struct KırmızıYeşilMavi {
    ubyte kırmızı;
    ubyte yeşil;
    ubyte mavi;
}

class SiteÇerçevesi : RenkliNesne!KırmızıYeşilMavi {
    void renklendir(KırmızıYeşilMavi renk) {
        // ...
    }
}

Öte yandan, renk olarak ışığın frekansını kullanmak isteyen bir sınıf, renk için frekans değerine uygun olan başka bir türden yararlanabilir:

alias Frekans = double;

class Lamba : RenkliNesne!Frekans {
    void renklendir(Frekans renk) {
        // ...
    }
}

Yine Şablonlar bölümünden hatırlayacağınız gibi, "her şablon gerçekleştirmesi farklı bir türdür". Buna göre, RenkliNesne!KırmızıYeşilMavi ve RenkliNesne!Frekans arayüzleri, farklı arayüzlerdir. Bu yüzden, onlardan türeyen sınıflar da birbirlerinden bağımsız sıradüzenlerin parçalarıdırlar; SiteÇerçevesi ve Lamba, birbirlerinden bağımsızdır.

Şablon parametre çeşitleri

Şimdiye kadar gördüğümüz şablonlar, hep türler konusunda serbestlik getiriyorlardı.

Yukarıdaki örneklerde de kullandığımız T ve RenkTürü gibi şablon parametreleri, hep türleri temsil ediyorlardı. Örneğin T'nin anlamı, şablonun kod içindeki kullanımına bağlı olarak int, double, Öğrenci, vs. gibi bir tür olabiliyordu.

Şablon parametreleri; değer, this, alias, ve çokuzlu da olabilirler.

Tür parametreleri

Bu alt başlığı bütünlük amacıyla yazdım.

Şimdiye kadar gördüğümüz bütün şablon parametreleri zaten hep tür parametreleriydi.

Değer parametreleri

Şablon parametresi olarak değerler de kullanılabilir. Bu, şablonun tanımı ile ilgili bir değerin serbest bırakılmasını sağlar.

Şablonlar derleme zamanı olanakları olduklarından, değer olarak kullanılan şablon parametresinin derleme zamanında hesaplanabilmesi şarttır. Bu yüzden, programın çalışması sırasında hesaplanan, örneğin girişten okunan bir değer kullanılamaz.

Bir örnek olarak, belirli sayıda köşeden oluşan şekilleri temsil eden yapılar tanımlayalım:

struct Üçgen {
    Nokta[3] köşeler;
// ...
}

struct Dörtgen {
    Nokta[4] köşeler;
// ...
}

struct Beşgen {
    Nokta[5] köşeler;
// ...
}

Örnek kısa olsun diye başka üyelerini göstermedim. Normalde, o türlerin başka üyelerinin ve işlevlerinin de bulunduğunu ve hepsinde tamamen aynı şekilde tanımlandıklarını varsayalım. Sonuçta, dizi uzunluğunu belirleyen değer dışında, o yapıların tanımları aynı olsun.

Değer şablon parametreleri böyle durumlarda yararlıdır. Yukarıdaki tanımlar yerine tek yapı şablonu tanımlanabilir. Yeni tanım genel amaçlı olduğu için, ismini de o şekillerin genel ismi olan poligon koyarak şöyle tanımlayabiliriz:

struct Poligon(size_t köşeAdedi) {
    Nokta[köşeAdedi] köşeler;
// ...
}

O yapı şablonu parametre olarak size_t türünde ve köşeAdedi isminde bir şablon parametresi almaktadır. O parametre değeri yapının tanımında herhangi bir yerde kullanılabilir.

Artık o şablonu istediğimiz sayıda köşesi olan poligonları ifade etmek için kullanabiliriz:

    auto yüzKöşeli = Poligon!100();

Yine alias'tan yararlanarak kullanışlı isimler verebiliriz:

alias Üçgen = Poligon!3;
alias Dörtgen = Poligon!4;
alias Beşgen = Poligon!5;

// ...

    auto üçgen = Üçgen();
    auto dörtgen = Dörtgen();
    auto beşgen = Beşgen();

Yukarıdaki değer şablon parametresinin türü size_t idi. Değer derleme zamanında bilindiği sürece değer türü olarak bütün temel türler, yapılar, diziler, dizgiler, vs. kullanılabilir.

struct S {
    int i;
}

// Türü S yapısı olan değer şablon parametresi
void foo(S s)() {
    // ...
}

void main() {
    // İşlev şablonunun S(42) hazır değeriyle kullanılması
    foo!(S(42))();
}

Başka bir örnek olarak, basit XML elemanları oluşturmakta kullanılan bir sınıf şablonu tasarlayalım. Bu basit XML tanımı, çok basitçe şu çıktıyı üretmek için kullanılsın:

Örneğin değeri 42 olan bir elemanın <isim>42</isim> şeklinde görünmesini isteyelim.

Eleman isimlerini bir sınıf şablonunun string türündeki bir değer parametresi olarak belirleyebiliriz:

import std.string;

class XmlElemanı(string isim) {
    double değer;

    this(double değer) {
        this.değer = değer;
    }

    override string toString() const {
        return format("<%s>%s</%s>", isim, değer, isim);
    }
}

Bu örnekteki şablon parametresi, şablonda kullanılan bir türle değil, bir string değeriyle ilgilidir. O string'in değeri de şablon içinde gereken her yerde kullanılabilir.

alias'tan yararlanarak kullanışlı tür isimleri de tanımlayarak:

alias Konum = XmlElemanı!"konum";
alias Sıcaklık = XmlElemanı!"sıcaklık";
alias Ağırlık = XmlElemanı!"ağırlık";

void main() {
    Object[] elemanlar;

    elemanlar ~= new Konum(1);
    elemanlar ~= new Sıcaklık(23);
    elemanlar ~= new Ağırlık(78);

    writeln(elemanlar);
}

Not: Ben bu örnekte kısa olsun diye ve nasıl olsa bütün sınıf sıradüzenlerinin en üstünde bulunduğu için bir Object dizisi kullandım. O sınıf şablonu aslında daha uygun bir arayüz sınıfından da türetilebilirdi.

Yukarıdaki kodun çıktısı:

[<konum>1</konum>, <sıcaklık>23</sıcaklık>, <ağırlık>78</ağırlık>]

Değer parametrelerinin de varsayılan değerleri olabilir. Örneğin, herhangi boyutlu bir uzaydaki noktaları temsil eden bir yapı tasarlayalım. Noktaların koordinat değerleri için kullanılan tür ve uzayın kaç boyutlu olduğu, şablon parametreleri ile belirlensin:

struct Konum(T, size_t boyut = 3) {
    T[boyut] koordinatlar;
}

boyut parametresinin varsayılan bir değerinin bulunması, bu şablonun o parametre belirtilmeden de kullanılabilmesini sağlar:

    Konum!double merkez;    // üç boyutlu uzayda bir nokta

Gerektiğinde farklı bir değer de belirtilebilir:

    Konum!(int, 2) nokta;   // iki boyutlu düzlemde bir nokta

Parametre Serbestliği bölümünde özel anahtar sözcüklerin varsayılan parametre değeri olarak kullanıldıklarında farklı etkileri olduğunu görmüştük.

Benzer biçimde, varsayılan şablon parametre değeri olarak kullanıldıklarında şablonun tanımlandığı yerle değil, şablonun kullanıldığı yerle ilgili bilgi verirler:

import std.stdio;

void işlev(T,
           string işlevİsmi = __FUNCTION__,
           string dosya = __FILE__,
           size_t satır = __LINE__)(T parametre) {
    writefln("%s dosyasının %s numaralı satırındaki %s " ~
             "işlevi tarafından kullanılıyor.",
             dosya, satır, işlevİsmi);
}

void main() {
    işlev(42);    // ← satır 13
}

Yukarıdaki özel anahtar sözcükler şablonun tanımında geçtikleri halde şablonu kullanmakta olan main() işlevine işaret ederler:

deneme.d dosyasının 13 numaralı satırındaki deneme.main
işlevi tarafından kullanılıyor.

__FUNCTION__ anahtar sözcüğünü aşağıdaki işleç yükleme örneğinde de kullanacağız.

Üye işlevler için this şablon parametreleri

Üye işlevler de şablon olarak tanımlanabilirler. Üye işlev şablonlarının da tür ve değer parametreleri bulunabilir, ve normal işlev şablonlarından beklendiği gibi çalışırlar.

Ek olarak, üye işlev şablonlarının parametreleri this anahtar sözcüğü ile de tanımlanabilir. Bu durumda, o anahtar sözcükten sonra yazılan isim, o nesnenin this referansının türü haline gelir. (Not: Burada, çoğunlukla kurucu işlevler içinde gördüğümüz this.üye = değer kullanımındaki this referansından, yani nesnenin kendisini ifade eden referanstan bahsediyoruz.)

struct BirYapı(T) {
    void birİşlev(this KendiTürüm)() const {
        writeln("Bu nesnenin türü: ", KendiTürüm.stringof);
    }
}

KendiTürüm şablon parametresi o üye işlevin işlemekte olduğu nesnenin asıl türüdür:

    auto m = BirYapı!int();
    auto c = const(BirYapı!int)();
    auto i = immutable(BirYapı!int)();

    m.birİşlev();
    c.birİşlev();
    i.birİşlev();

Çıktısı:

Bu nesnenin türü: BirYapı!int
Bu nesnenin türü: const(BirYapı!int)
Bu nesnenin türü: immutable(BirYapı!int)

Görüldüğü gibi, KendiTürüm hem T'nin bu kullanımda int olan karşılığını hem de const ve immutable gibi tür belirteçlerini içerir.

this şablon parametresi, şablon olmayan yapıların veya sınıfların üye işlevlerinde de kullanılabilir.

this şablon parametreleri özellikle iki bölüm sonra göreceğimiz şablon katmalarında yararlıdır. O bölümde bir örneğini göreceğiz.

alias parametreleri

alias şablon parametrelerine karşılık olarak D programlarında geçebilen bütün yasal isimler veya ifadeler kullanılabilir. Bu isimler yerel isimler, modül isimleri, başka şablon isimleri, vs. olabilirler. Tek koşul, o parametrenin şablon içindeki kullanımının o parametreye uygun olmasıdır.

Bu olanak, filter ve map gibi şablonların da işlemleri dışarıdan almalarını sağlayan olanaktır.

Örnek olarak, hangi yerel değişkeni değiştireceği kendisine bir alias parametre olarak bildirilen bir yapıya bakalım:

struct BirYapı(alias değişken) {
    void birİşlev(int değer) {
        değişken = değer;
    }
}

O yapının üye işlevi, değişken isminde bir değişkene bir atama yapmaktadır. O değişkenin programdaki hangi değişken olduğu; bu şablon tanımlandığı zaman değil, şablon kullanıldığı zaman belirlenir:

    int x = 1;
    int y = 2;

    auto nesne = BirYapı!x();
    nesne.birİşlev(10);
    writeln("x: ", x, ", y: ", y);

Yapı şablonunun yukarıdaki kullanımında yerel x değişkeni belirtildiği için, birİşlev içindeki atama onu etkiler:

x: 10, y: 2

Öte yandan, BirYapı!y biçimindeki kullanımda değişken değişkeni y yerine geçerdi.

Başka bir örnek olarak, filter ve map gibi alias parametresini işlev olarak kullanan bir işlev şablonuna bakalım:

void çağıran(alias işlev)() {
    write("çağırıyorum: ");
    işlev();
}

() parantezlerinden anlaşıldığı gibi, çağıran ismindeki işlev şablonu, kendisine verilen parametreyi bir işlev olarak kullanmaktadır. Ayrıca, parantezlerin içinin boş olmasından anlaşıldığı gibi, o işlev parametre göndermeden çağrılmaktadır.

Parametre almadıkları için o kullanıma uyan iki de işlev bulunduğunu varsayalım:

void birinci() {
    writeln("birinci");
}

void ikinci() {
    writeln("ikinci");
}

O işlevler, çağıran şablonu içindeki kullanıma uydukları için o şablonun alias parametresinin değeri olabilirler:

    çağıran!birinci();
    çağıran!ikinci();

Belirtilen işlevin çağrıldığını görürüz:

çağırıyorum: birinci
çağırıyorum: ikinci

alias şablon parametrelerini her çeşit şablonla kullanabilirsiniz. Önemli olan, o parametrenin şablon içindeki kullanıma uymasıdır. Örneğin, yukarıdaki alias parametresi yerine bir değişken kullanılması derleme hatasına neden olacaktır:

    int değişken;
    çağıran!değişken();        // ← derleme HATASI

Aldığımız hata, () karakterlerinden önce bir işlev beklendiğini, int türündeki değişken'in uygun olmadığını belirtir:

Error: function expected before (), not değişken of type int

Her ne kadar işaretlediğim satır nedeniyle olsa da, derleme hatası aslında çağıran işlevinin içindeki işlev() satırı için verilir. Derleyicinin gözünde hatalı olan; şablona gönderilen parametre değil, o parametrenin şablondaki kullanılışıdır. Uygunsuz şablon parametrelerini önlemenin bir yolu, şablon kısıtlamaları tanımlamaktır. Bunu aşağıda göreceğiz.

Öte yandan, bir işlev gibi çağrılabilen her olanak bu örnekteki alias parametresi yerine kullanılabilir. Aşağıda hem opCall() işlecini yüklemiş olan bir sınıf ile hem de bir isimsiz işlev ile kullanımını görüyoruz:

class Sınıf {
    void opCall() {
        writeln("Sınıf.opCall çağrıldı.");
    }
}

// ...

    auto nesne = new Sınıf();
    çağıran!nesne();

    çağıran!({ writeln("İsimsiz işlev çağrıldı."); })();

Çıktısı:

çağırıyorum: Sınıf.opCall çağrıldı.
çağırıyorum: İsimsiz işlev çağrıldı.

alias parametreleri de özellenebilirler. Ancak, özelleme söz dizimi diğer parametre çeşitlerinden farklıdır; özellenen tür alias ile parametre ismi arasına yazılır:

import std.stdio;

void foo(alias değişken)() {
    writefln("Genel tanım %s türündeki '%s' değişkeni için işliyor.",
             typeof(değişken).stringof, değişken.stringof);
}

void foo(alias int i)() {
    writefln("int özellemesi '%s' değişkeni için işliyor.",
             i.stringof);
}

void foo(alias double d)() {
    writefln("double özellemesi '%s' değişkeni için işliyor.",
             d.stringof);
}

void main() {
    string isim;
    foo!isim();

    int adet;
    foo!adet();

    double uzunluk;
    foo!uzunluk();
}

Asıl değişkenlerin isimlerinin de şablon içinde görülebildiklerine ayrıca dikkat edin:

Genel tanım string türündeki 'isim' değişkeni için işliyor.
int özellemesi 'adet' değişkeni için işliyor.
double özellemesi 'uzunluk' değişkeni için işliyor.
Çokuzlu parametreleri

İşlevlerin belirsiz sayıda parametre alabildiklerini biliyoruz. Örneğin, writeln işlevini istediğimiz sayıda parametre ile çağırabiliriz. Bu tür işlevlerin nasıl tanımlandıklarını Parametre Serbestliği bölümünde görmüştük.

Aynı serbestlik şablon parametrelerinde de bulunur. Şablon parametrelerinin sayısını ve çeşitlerini serbest bırakmak şablon parametre listesinin en sonuna bir çokuzlu ismi ve ... karakterleri yazmak kadar basittir. İsmi belirtilen çokuzlu, şablon parametre değerlerini ifade eden bir AliasSeq gibi kullanılabilir.

Bunu parametreleri hakkında bilgi veren basit bir işlev şablonunda görelim:

void bilgiVer(T...)(T parametreler) {
    // ...
}

Şablon parametresi olan T..., bilgiVer işlev şablonunun belirsiz sayıda parametre ile çağrılabilmesini sağlar. Hem T hem de parametreler çokuzludur:

İşlevin üç farklı türden parametre ile çağrıldığı duruma bakalım:

import std.stdio;

// ...

void main() {
    bilgiVer(1, "abc", 2.3);
}

Aşağıda parametreler'in foreach ile kullanımını görüyoruz:

void bilgiVer(T...)(T parametreler) {
    // 'parametreler' bir AliasSeq gibi kullanılır:
    foreach (i, parametre; parametreler) {
        writefln("%s: %s türünde %s",
                 i, typeof(parametre).stringof, parametre);
    }
}

Not: Bir önceki bölümde gördüğümüz gibi, parametre değerleri çokuzlu olduğundan, yukarıdaki foreach bir derleme zamanı foreach'idir.

Çıktısı:

0: int türünde 1
1: string türünde abc
2: double türünde 2.3

Parametrelerin türleri typeof(parametre) yerine T[i] ile de edinilebilir.

İşlev şablonu parametre türlerinin derleyici tarafından çıkarsanabildiklerini biliyorsunuz. Yukarıdaki bilgiVer() çağrısı sırasında parametre değerlerine bakılarak onların türlerinin sırasıyla int, string, ve double oldukları derleyici tarafından çıkarsanmıştır.

Bazı durumlarda ise şablon parametrelerinin açıkça belirtilmeleri gerekebilir. Örneğin, std.conv.to şablonu hedef türü açıkça belirtilmesi gereken bir şablon parametresi olarak alır:

    to!string(42);

Şablon parametreleri açıkça belirtildiğinde o parametreler değer, tür, veya başka çeşitlerden karışık olabilirler. Öyle durumlarda her şablon parametresinin tür veya başka çeşitten olup olmadığının belirlenmesi ve şablon kodlarının buna uygun olarak yazılması gerekebilir. Şablon çeşitlerini ayırt etmenin yolu, parametreleri yine AliasSeq gibi kullanmaktır.

Bunun örneğini görmek için yapı tanımı üreten bir işlev tasarlayalım. Bu işlev belirtilen türlerde ve isimlerde üyeleri olan yapı tanımı içeren kaynak kod üretsin ve string olarak döndürsün. İlk olarak yapının ismi verildikten sonra üyelerin tür ve isimleri çiftler halinde belirtilsinler:

import std.stdio;

void main() {
    writeln(yapıTanımla!("Öğrenci",
                         string, "isim",
                         int, "numara",
                         int[], "notlar")());
}

Yukarıdaki programın çıktısı aşağıdaki kaynak kod olmalıdır:

struct Öğrenci {
    string isim;
    int numara;
    int[] notlar;
}

Not: yapıTanımla gibi kod üreten işlevlerin yararlarını daha sonraki bir bölümdeki mixin anahtar sözcüğünü öğrenirken göreceğiz.

O sonucu üreten şablon aşağıdaki gibi tanımlanabilir. Koddaki denetimlerde is ifadesinden de yararlanıldığına dikkat edin. Hatırlarsanız, is (parametre) ifadesi parametre geçerli bir tür olduğunda true üretiyordu:

import std.string;

string yapıTanımla(string isim, Üyeler...)() {
    /* Üyeler tür ve isim olarak çiftler halinde
     * belirtilmelidir. Önce bunu denetleyelim. */
    static assert((Üyeler.length % 2) == 0,
                  "Üyeler çiftler halinde belirtilmedilir.");

    /* Önce yapı tanımının ilk satırını oluşturuyoruz. */
    string sonuç = "struct " ~ isim ~ "\n{\n";

    foreach (i, parametre; Üyeler) {
        static if (i % 2) {
            /* Tek numaralı parametreler üye isimlerini
             * belirliyorlar. Onları hemen burada kullanmak
             * yerine aşağıdaki 'else' bölümünde Üyeler[i+1]
             * söz dizimiyle kullanacağız.
             *
             * Yine de üye isimlerinin string olarak
             * belirtildiklerini burada denetleyelim. */
            static assert(is (typeof(parametre) == string),
                          "Üye ismi " ~ parametre.stringof ~
                          " string değil.");

        } else {
            /* Bu durumda 'parametre' üyenin türünü
             * belirtiyor. Öncelikle bu parametrenin gerçekten
             * bir tür olduğunu denetleyelim. */
            static assert(is (parametre),
                          parametre.stringof ~ " tür değil.");

            /* Tür ve üye isimlerini kullanarak satırı
             * oluşturuyoruz.
             *
             * Not: Burada Üyeler[i] yerine 'parametre' de
             * yazabilirdik. */
            sonuç ~= format("    %s %s;\n",
                            Üyeler[i].stringof, Üyeler[i+1]);
        }
    }

    /* Yapı tanımının kapama parantezi. */
    sonuç ~= "}";

    return sonuç;
}

import std.stdio;

void main() {
    writeln(yapıTanımla!("Öğrenci",
                         string, "isim",
                         int, "numara",
                         int[], "notlar")());
}
typeof(this), typeof(super), ve typeof(return)

Şablonların genel tanımlar ve genel algoritmalar olmalarının bir etkisi, bazı tür isimlerinin yazımlarının güç veya olanaksız olmasıdır. Aşağıdaki üç özel typeof çeşidi böyle durumlarda yararlıdır. Her ne kadar bu bölümde tanıtılıyor olsalar da, bu olanaklar şablon olmayan kodlarda da geçerlidirler.

Şablon özellemeleri

Özellemeleri de Şablonlar bölümünde görmüştük. Aşağıdaki meta programlama başlığında da özelleme örnekleri göreceksiniz.

Tür parametrelerinde olduğu gibi, başka çeşit şablon parametreleri de özellenebilir. Aşağıdaki şablonun hem genel hem de 0 değeri için özel tanımı görülüyor:

void birİşlev(int birDeğer)() {
    // ... genel tanımı ...
}

void birİşlev(int birDeğer : 0)() {
    // ... sıfıra özel tanımı ...
}
Meta programlama

Kod üretme ile ilgili olduklarından şablonlar diğer D olanaklarından daha üst düzey programlama araçları olarak kabul edilirler. Şablonlar bir anlamda kod oluşturan kodlardır. Kodların daha üst düzey kodlarla oluşturulmaları kavramına meta programlama denir.

Şablonların derleme zamanı olanakları olmaları normalde çalışma zamanında yapılan işlemlerin derleme zamanına taşınmalarına olanak verir. (Not: Aynı amaçla Derleme Zamanında İşlev İşletme (CTFE) olanağı da kullanılabilir. Bu konuyu daha sonraki bir bölümde göreceğiz.)

Şablonların bu amaçla derleme zamanında işletilmeleri, çoğunlukla özyineleme üzerine kuruludur.

Bunun bir örneğini görmek için 0'dan başlayarak belirli bir sayıya kadar olan bütün sayıların toplamını hesaplayan normal bir işlev düşünelim. Bu işlev, parametre olarak örneğin 4 aldığında 0+1+2+3+4'ün toplamını döndürsün:

int topla(int sonDeğer) {
    int sonuç = 0;

    foreach (değer; 0 .. sonDeğer + 1) {
        sonuç += değer;
    }

    return sonuç;
}

Aynı işlevi özyinelemeli olarak da yazabiliriz:

int topla(int sonDeğer) {
    return (sonDeğer == 0
            ? sonDeğer
            : sonDeğer + topla(sonDeğer - 1));
}

Özyinelemeli işlev kendi düzeyindeki değeri bir önceki hesaba eklemektedir. İşlevde 0 değerinin özel olarak kullanıldığını görüyorsunuz; özyineleme onun sayesinde sonlanmaktadır.

İşlevlerin normalde çalışma zamanı olanakları olduklarını biliyoruz. topla'yı çalışma zamanında gerektikçe çağırabilir ve sonucunu kullanabiliriz:

    writeln(topla(4));

Aynı sonucun yalnızca derleme zamanında gerektiği durumlarda ise, o hesap bir işlev şablonuyla da gerçekleştirilebilir. Yapılması gereken; değerin işlev parametresi olarak değil, şablon parametresi olarak kullanılmasıdır:

// Uyarı: Bu kod yanlıştır
int topla(int sonDeğer)() {
    return (sonDeğer == 0
            ? sonDeğer
            : sonDeğer + topla!(sonDeğer - 1)());
}

Bu şablon da hesap sırasında kendisinden yararlanmaktadır. Kendisini, sonDeğer'in bir eksiği ile kullanmakta ve hesabı yine özyinelemeli olarak elde etmeye çalışmaktadır. Ne yazık ki o kod yazıldığı şekilde çalışamaz.

Derleyici, ?: işlecini çalışma zamanında işleteceği için, yukarıdaki özyineleme derleme zamanında sonlanamaz:

    writeln(topla!4());    // ← derleme HATASI

Derleyici, aynı şablonun sonsuz kere dallandığını anlar ve bir hata ile sonlanır:

Error: template instance deneme.topla!(-296) recursive expansion

Şablon parametresi olarak verdiğimiz 4'ten geriye doğru -296'ya kadar saydığına bakılırsa, derleyici şablonların özyineleme sayısını 300 ile sınırlamaktadır.

Meta programlamada özyinelemeyi kırmanın yolu, şablon özellemeleri kullanmaktır. Bu durumda, aynı şablonu 0 değeri için özelleyebilir ve özyinelemenin kırılmasını bu sayede sağlayabiliriz:

// Genel tanım
int topla(int sonDeğer)() {
    return sonDeğer + topla!(sonDeğer - 1)();
}

// Sıfır değeri için özellemesi
int topla(int sonDeğer : 0)() {
    return 0;
}

Derleyici, sonDeğer'in sıfırdan farklı değerleri için hep genel tanımı kullanır ve en sonunda 0 değeri için özel tanıma geçer. O tanım da basitçe 0 değerini döndürdüğü için özyineleme sonlanmış olur.

O işlev şablonunu şöyle bir programla deneyebiliriz:

import std.stdio;

void main() {
    writeln(topla!4());
}

Şimdi hatasız olarak derlenecek ve 4+3+2+1+0'ın toplamını üretecektir:

10

Burada dikkatinizi çekmek istediğim önemli nokta, topla!4() işlevinin bütünüyle derleme zamanında işletiliyor olmasıdır. Sonuçta derleyicinin ürettiği kod, writeln'e doğrudan 10 hazır değerini göndermenin eşdeğeridir:

    writeln(10);         // topla!4()'lü kodun eşdeğeri

Derleyicinin ürettiği kod, 10 hazır değerini doğrudan programa yazmak kadar hızlı ve basittir. O 10 hazır değeri, yine de 4+3+2+1+0 hesabının sonucu olarak bulunmaktadır; ancak o hesap, şablonların özyinelemeli olarak kullanılmalarının sonucunda derleme zamanında işletilmektedir.

Burada görüldüğü gibi, meta programlamanın yararlarından birisi, şablonların derleme zamanında işletilmelerinden yararlanarak normalde çalışma zamanında yapılmasına alıştığımız hesapların derleme zamanına taşınabilmesidir.

Yukarıda da söylediğim gibi, daha sonraki bir bölümde göstereceğim CTFE olanağı bazı meta programlama yöntemlerini D'de gereksiz hale getirir.

Derleme zamanı çok şekilliliği

Bu kavram, İngilizce'de "compile time polymorphism" olarak geçer.

Nesne yönelimli programlamada çok şekilliliğin sınıf türetme ile sağlandığını biliyorsunuz. Örneğin bir işlev parametresinin bir arayüz olması, o parametre yerine o arayüzden türemiş her sınıfın kullanılabileceği anlamına gelir.

Daha önce gördüğümüz bir örneği hatırlayalım:

import std.stdio;

interface SesliAlet {
    string ses();
}

class Keman : SesliAlet {
    string ses() {
        return "♩♪♪";
    }
}

class Çan : SesliAlet {
    string ses() {
        return "çın";
    }
}

void sesliAletKullan(SesliAlet alet) {
    // ... bazı işlemler ...
    writeln(alet.ses());
    // ... başka işlemler ...
}

void main() {
    sesliAletKullan(new Keman);
    sesliAletKullan(new Çan);
}

Yukarıdaki sesliAletKullan işlevi çok şekillilikten yararlanmaktadır. Parametresi SesliAlet olduğu için, ondan türemiş olan bütün türlerle kullanılabilir.

Yukarıdaki son cümlede geçen bütün türlerle kullanılabilme kavramını şablonlardan da tanıyoruz. Böyle düşününce, şablonların da bir çeşit çok şekillilik sunduklarını görürüz. Şablonlar bütünüyle derleyicinin derleme zamanındaki kod üretmesiyle ilgili olduklarından, şablonların sundukları çok şekilliliğe derleme zamanı çok şekilliliği denir.

Doğrusu, her iki çok şekillilik de bütün türlerle kullanılamaz. Her ikisinde de türlerin uymaları gereken bazı koşullar vardır.

Çalışma zamanı çok şekilliliği, belirli bir arayüzden türeme ile kısıtlıdır.

Derleme zamanı çok şekilliliği ise şablon içindeki kullanıma uyma ile kısıtlıdır. Şablon parametresi, şablon içindeki kullanımda derleme hatasına neden olmuyorsa, o şablonla kullanılabilir. (Not: Eğer tanımlanmışsa, şablon kısıtlamalarına da uyması gerekir. Şablon kısıtlamalarını biraz aşağıda anlatacağım.)

Örneğin, yukarıdaki sesliAletKullan işlevi bir şablon olarak yazıldığında, ses üye işlevi bulunan bütün türlerle kullanılabilir:

void sesliAletKullan(T)(T alet) {
    // ... bazı işlemler ...
    writeln(alet.ses());
    // ... başka işlemler ...
}

class Araba {
    string ses() {
        return "düt düt";
    }
}

// ...

    sesliAletKullan(new Keman);
    sesliAletKullan(new Çan);
    sesliAletKullan(new Araba);

Yukarıdaki şablon, diğerleriyle kalıtım ilişkisi bulunmayan Araba türü ile de kullanılabilmiştir.

Derleme zamanı çok şekilliliği ördek tipleme olarak da bilinir. Ördek tipleme, asıl türü değil, davranışı ön plana çıkartan mizahi bir terimdir.

Kod şişmesi

Şablonlar kod üretme ile ilgilidirler. Derleyici, şablonun farklı parametrelerle her kullanımı için farklı kod üretir.

Örneğin yukarıda en son yazdığımız sesliAletKullan işlev şablonu, programda kullanıldığı her tür için ayrı ayrı üretilir ve derlenir. Programda yüz farklı tür ile çağrıldığını düşünürsek; derleyici o işlev şablonunun tanımını, her tür için ayrı ayrı, toplam yüz kere oluşturacaktır.

Programın boyutunun büyümesine neden olduğu için bu etkiye kod şişmesi (code bloat) denir. Çoğu programda sorun oluşturmasa da, şablonların bu özelliğinin akılda tutulması gerekir.

Öte yandan, sesliAletKullan işlevinin ilk yazdığımız SesliAlet alan tanımında, yani şablon olmayan tanımında, böyle bir kod tekrarı yoktur. Derleyici, o işlevi bir kere derler ve her SesliAlet türü için aynı işlevi çağırır. İşlev tek olduğu halde her hayvanın kendisine özel olarak davranabilmesi, derleyici tarafından işlev göstergeleriyle sağlanır. Derleyici her tür için farklı bir işlev göstergesi kullanır ve böylece her tür için farklı üye işlev çağrılır. Çalışma zamanında çok küçük bir hız kaybına yol açsa da, işlev göstergeleri kullanmanın çoğu programda önemi yoktur ve zaten bu çözümü sunan en hızlı gerçekleştirmedir.

Burada sözünü ettiğim hız etkilerini tasarımlarınızda fazla ön planda tutmayın. Program boyutunun artması da, çalışma zamanında fazladan işlemler yapılması da hızı düşürecektir. Belirli bir programda hangisinin etkisinin daha fazla olduğuna ancak o program denenerek karar verilebilir.

Şablon kısıtlamaları

Şablonların her tür ve değerdeki şablon parametresi ile çağrılabiliyor olmalarının getirdiği bir sorun vardır. Uyumsuz bir parametre kullanıldığında, bu uyumsuzluk ancak şablonun kendi kodları derlenirken farkedilebilir. Bu yüzden, derleme hatasında belirtilen satır numarası, şablon bloğuna işaret eder.

Yukarıdaki sesliAletKullan şablonunu ses isminde üye işlevi bulunmayan bir türle çağıralım:

class Fincan {
    // ... ses() işlevi yok ...
}

// ...

    sesliAletKullan(new Fincan);   // ← uyumsuz bir tür

Oradaki hata, şablonun uyumsuz bir türle çağrılıyor olmasıdır. Oysa derleme hatası, şablon içindeki kullanıma işaret eder:

void sesliAletKullan(T)(T alet) {
    // ... bazı işlemler ...
    writeln(alet.ses());          // ← derleme HATASI
    // ... başka işlemler ...
}

Bunun bir sakıncası, belki de bir kütüphane modülünde tanımlı olan bir şablona işaret edilmesinin, hatanın o kütüphanede olduğu yanılgısını uyandırabileceğidir. Daha önemlisi, asıl hatanın hangi satırda olduğunun hiç bildirilmiyor olmasıdır.

Böyle bir sorunun arayüzlerde bulunmadığına dikkat edin. Parametre olarak arayüz alacak şekilde yazılmış olan bir işlev, ancak o arayüzden türemiş olan türlerle çağrılabilir. Türeyen her tür arayüzün işlevlerini gerçekleştirmek zorunda olduğu için, işlevin uyumsuz bir türle çağrılması olanaksızdır. O durumda derleme hatası, işlevi uygunsuz türle çağıran satıra işaret eder.

Şablonların yalnızca belirli koşulları sağlayan türlerle kullanılmaları şablon kısıtlamaları ile sağlanır. Şablon kısıtlamaları, şablon bloğunun hemen öncesine yazılan if deyiminin içindeki mantıksal ifadelerdir:

void birŞablon(T)()
        if (/* ... kısıtlama koşulu ... */) {
    // ... şablonun tanımı ...
}

Derleyici bu şablon tanımını ancak kısıtlama koşulu true olduğunda göze alır. Koşulun false olduğu durumda ise bu şablon tanımını gözardı eder.

Şablonlar derleme zamanı olanakları olduklarından şablon kısıtlamaları da derleme zamanında işletilirler. Bu yüzden, is İfadesi bölümünde gördüğümüz ve derleme zamanında işletildiğini öğrendiğimiz is ifadesi ile de çok kullanılırlar. Bunun örneklerini aşağıda göstereceğim.

Tek üyeli çokuzlu parametre yöntemi

Bazen tek şablon parametresi gerekebilir ama o parametrenin tür, değer, veya alias çeşidinden olabilmesi istenir. Bunu sağlamanın bir yolu, çokuzlu çeşidinde parametre kullanmak ama çokuzlunun uzunluğunu bir şablon kısıtlaması ile tek olarak belirlemektir:

template birŞablon(T...)
        if (T.length == 1) {
    static if (is (T[0])) {
        // Şablonun tek parametresi bir türmüş
        enum bool birŞablon = /* ... */;

    } else {
        // Şablonun tek parametresi tür değilmiş
        enum bool birŞablon = /* ... */;
    }
}

Daha ileride göreceğimiz std.traits modülündeki bazı şablonlar bu yöntemden yararlanır.

İsimli kısıtlama yöntemi

Şablon kısıtlamaları bazı durumlarda yukarıdakinden çok daha karmaşık olabilirler. Bunun üstesinden gelmenin bir yolu, benim isimli kısıtlama olarak adlandırdığım bir yöntemdir. Bu yöntem D'nin dört olanağından yararlanarak kısıtlamaya anlaşılır bir isim verir. Bu dört olanak; isimsiz işlev, typeof, is ifadesi, ve tek tanım içeren şablonlardır.

Bu yöntemi burada daha çok bir kalıp olarak göstereceğim ve her ayrıntısına girmemeye çalışacağım.

Parametresini belirli şekilde kullanan bir işlev şablonu olsun:

void kullan(T)(T nesne) {
    // ...
    nesne.hazırlan();
    // ...
    nesne.uç(42);
    // ...
    nesne.kon();
    // ...
}

Şablon içindeki kullanımından anlaşıldığı gibi, bu şablonun kullanıldığı türlerin hazırlan, , ve kon isminde üç üye işlevinin bulunması gerekir (UFCS olanağı sayesinde normal işlevler de olabilirler.) O işlevlerden 'un ayrıca int türünde bir de parametresi olmalıdır.

Bu kısıtlamayı is ve typeof ifadelerinden yararlanarak şöyle yazabiliriz:

void kullan(T)(T nesne)
        if (is (typeof(nesne.hazırlan())) &&
            is (typeof(nesne.uç(1))) &&
            is (typeof(nesne.kon()))) {
    // ...
}

O koşulun anlamını aşağıda daha ayrıntılı olarak göreceğiz. Şimdilik is (typeof(nesne.hazırlan())) kullanımını bir kalıp olarak eğer o tür nesne.hazırlan() çağrısını destekliyorsa anlamında kabul edebilirsiniz. İşleve is (typeof(nesne.uç(1))) biçiminde parametre verildiğinde ise, o işlev int türünde parametre de alıyorsa diye kabul edebilirsiniz.

Yukarıdaki gibi bir kısıtlama istendiği gibi çalışıyor olsa da, her zaman için tam açık olmayabilir. Onun yerine, o şablon kısıtlamasının ne anlama geldiğini daha iyi açıklayan bir isim verilebilir:

void kullan(T)(T nesne)
        if (uçabilir_mi!T) {     // ← isimli kısıtlama
    // ...
}

Bu kısıtlama bir öncekinden daha açıktır. Bu şablonun uçabilen türlerle çalıştığını okunaklı bir şekilde belgeler.

Yukarıdaki gibi isimli kısıtlamalar şu kalıba uygun olarak tanımlanırlar:

template uçabilir_mi(T) {
    enum uçabilir_mi = is (typeof(
    {
        T uçan;
        uçan.hazırlan();   // uçmaya hazırlanabilmeli
        uçan.uç(1);        // belirli mesafe uçabilmeli
        uçan.kon();        // istendiğinde konabilmeli
    }()));
}

O yöntemde kullanılan D olanaklarını ve birbirleriyle nasıl etkileştiklerini çok kısaca göstermek istiyorum:

template uçabilir_mi(T) {
    //      (6)        (5)  (4)
    enum uçabilir_mi = is (typeof(
    { // (1)
        T uçan;          // (2)
        uçan.hazırlan();
        uçan.uç(1);
        uçan.kon();
 // (3)
    }()));
}
  1. İsimsiz işlev: İsimsiz işlevleri İşlev Göstergeleri, İsimsiz İşlevler, ve Temsilciler bölümünde görmüştük. İşaretli olarak gösterilmiş olan yukarıdaki blok parantezleri, isimsiz bir işlev tanımlar.
  2. İşlev bloğu: İşlev bloğu, kısıtlaması tanımlanmakta olan türü asıl şablonda kullanıldığı gibi kullanır. Yukarıdaki blokta önce bu türden bir nesne oluşturulmakta ve o türün sahip olması gereken üç üye işlevi çağrılmaktadır. (Not: Bu kodlar typeof tarafından kullanılırlar ama hiçbir zaman işletilmezler.)
  3. İşlevin işletilmesi: Bir işlevin sonuna yazılan () parantezleri normalde o işlevi işletir. Ancak, yukarıdaki işletme bir typeof içinde olduğundan bu işlev hiçbir zaman işletilmez. (Bu, bir sonraki maddede açıklanıyor.)
  4. typeof ifadesi: typeof, şimdiye kadarki örneklerde çok kullandığımız gibi, kendisine verilen ifadenin türünü üretir.

    typeof'un önemli bir özelliği, türünü ürettiği ifadeyi işletmemesidir. typeof, bir ifadenin eğer işletilse ne türden bir değer üreteceğini bildirir:

        int i = 42;
        typeof(++i) j;    // 'int j;' yazmakla aynı anlamdadır
    
        assert(i == 42);  // ++i işletilmemiştir
    

    Yukarıdaki assert'ten de anlaşıldığı gibi, ++i ifadesi işletilmemiştir. typeof, yalnızca o ifadenin türünü üretmiş ve böylece j de int olarak tanımlanmıştır.

    Eğer typeof'a verilen ifadenin geçerli bir türü yoksa, typeof void bile olmayan geçersiz bir tür döndürür.

    Eğer uçabilir_mi şablonuna gönderilen tür, o isimsiz işlev içindeki kodlarda gösterildiği gibi derlenebiliyorsa, typeof geçerli bir tür üretir. Eğer o tür işlev içindeki kodlardaki gibi derlenemiyorsa, typeof geçersiz bir tür döndürür.

  5. is ifadesi: is İfadesi bölümünde is ifadesinin birden fazla kullanımını görmüştük. Buradaki is (Tür) şeklindeki kullanımı, kendisine verilen türün anlamlı olduğu durumda true değerini üretir:
        int i;
        writeln(is (typeof(i)));                  // true
        writeln(is (typeof(varOlmayanBirİsim)));  // false
    

    Yukarıdaki ikinci ifadede bilinmeyen bir isim kullanıldığı halde derleyici hata vermez. Programın çıktısı ikinci satır için false değerini içerir:

    true
    false
    

    Bunun nedeni, typeof'un ikinci kullanım için geçersiz bir tür üretmiş olmasıdır.

  6. Tek tanım içeren şablon: Daha yukarıda anlatıldığı gibi, uçabilir_mi şablonunun içinde tek tanım bulunduğundan ve o tanımın ismi şablonun ismiyle aynı olduğundan; uçabilir_mi şablonu, içerdiği uçabilir_mi enum sabit değeri yerine geçer.

Sonuçta, yukarıdaki kullan işlev şablonu bütün bu olanaklar sayesinde isimli bir kısıtlama edinmiş olur:

void kullan(T)(T nesne)
        if (uçabilir_mi!T) {
    // ...
}

O şablonu birisi uyumlu, diğeri uyumsuz iki türle çağırmayı deneyelim:

// Şablondaki kullanıma uyan bir tür
class ModelUçak {
    void hazırlan() {
    }

    void uç(int mesafe) {
    }

    void kon() {
    }
}

// Şablondaki kullanıma uymayan bir tür
class Güvercin {
    void uç(int mesafe) {
    }
}

// ...

    kullan(new ModelUçak);      // ← derlenir
    kullan(new Güvercin);       // ← derleme HATASI

İsimli veya isimsiz, bir şablon kısıtlaması tanımlanmış olduğundan, bu derleme hatası artık şablonun içine değil şablonun uyumsuz türle kullanıldığı satıra işaret eder.

Şablonların çok boyutlu işleç yüklemedeki kullanımı

opDollar, opIndex, ve opSlice işlevlerinin eleman erişimi ve dilimleme amacıyla kullanıldıklarını İşleç Yükleme bölümünde görmüştük. Bu işlevler o bölümdeki gibi tek boyutlu kullanımlarında aşağıdaki görevleri üstlenirler:

O işlevlerin şablon olarak yüklenebilen çeşitleri de vardır. Bu işlev şablonlarının anlamları yukarıdakilerden farklıdır. Özellikle opSlice'ın görevinin opIndex tarafından üstlenilmiş olduğuna dikkat edin:

opIndexAssign ve opIndexOpAssign'ın da şablon çeşitleri vardır. Bunlar da belirli bir alt topluluktaki elemanlar üzerinde işlerler.

Çok boyutlu işleçleri tanımlayan türler aşağıdaki gibi çok boyutlu erişim ve dilimleme söz dizimlerinde kullanılabilirler:

            // İndekslerle belirlenen alt topluluktaki
            // elemanların değerlerini 42 yapar:
            m[a, b..c, $-1, d..e] = 42;
//            ↑   ↑     ↑    ↑
// boyutlar:  0   1     2    3

Öyle bir ifade görüldüğünde önce $ karakterleri için opDollar ve konum aralıkları için opSlice perde arkasında otomatik olarak çağrılır. Elde edilen uzunluk ve aralık bilgileri yine otomatik olarak opIndexAssign'a parametre olarak geçirilir. Sonuçta, yukarıdaki ifade yerine aşağıdaki ifade işletilmiş olur (boyut değerleri işaretlenmiş olarak gösteriliyor):

    // Üsttekinin eşdeğeri:
    m.opIndexAssign(
        42,                    // ← atanan değer
        a,                     // ← sıfırıncı boyutun parametresi
        m.opSlice!1(b, c),     // ← birinci boyutun parametresi
        m.opDollar!2() - 1,    // ← ikinci boyutun parametresi
        m.opSlice!3(d, e));    // ← üçüncü boyutun parametresi

Sonuçta, opIndexAssign işlemde kullanacağı alt aralığı çokuzlu şablon parametrelerinin türlerine ve değerlerine bakarak belirler.

İşleç yükleme örneği

Aşağıdaki Matris türü bu işleçlerin nasıl tanımlandıklarının bir örneğini içeriyor.

Bu örnek çok daha hızlı işleyecek biçimde de gerçekleştirilebilir. Örneğin, aşağıdaki kodun tek elemana m[i, j] biçiminde erişirken bile tek elemanlı bir alt matris oluşturması gereksiz kabul edilebilir.

Ek olarak, işlev başlarındaki writeln(__FUNCTION__) ifadelerinin kodun işlevselliğiyle bir ilgisi bulunmuyor. Onlar yalnızca perde arkasında hangi işlevlerin hangi sırada çağrıldıklarını göstermek amacıyla eklenmişlerdir.

Boyut değerlerini denetlemek için şablon kısıtlamalarından yararlanıldığına da dikkat edin.

import std.stdio;
import std.format;
import std.string;

/* İki boyutlu bir int dizisi gibi işler. */
struct Matris {
private:

    int[][] satırlar;

    /* İndekslerle belirlenen satır ve sütun aralığı bilgisini
     * bir araya getirir. */
    struct Aralık {
        size_t baş;
        size_t son;
    }

    /* Satır ve sütun aralıklarıyla belirlenen alt matrisi
     * döndürür. */
    Matris altMatris(Aralık satırAralığı, Aralık sütunAralığı) {
        writeln(__FUNCTION__);

        int[][] dilimler;

        foreach (satır; satırlar[satırAralığı.baş ..
                                 satırAralığı.son]) {

            dilimler ~= satır[sütunAralığı.baş ..
                              sütunAralığı.son];
        }

        return Matris(dilimler);
    }

public:

    this(size_t yükseklik, size_t genişlik) {
        writeln(__FUNCTION__);

        satırlar = new int[][](yükseklik, genişlik);
    }

    this(int[][] satırlar) {
        writeln(__FUNCTION__);

        this.satırlar = satırlar;
    }

    void toString(void delegate(const(char)[]) hedef) const {
        hedef.formattedWrite!"%(%(%5s %)\n%)"(satırlar);
    }

    /* Belirtilen değeri matrisin bütün elemanlarına atar. */
    Matris opAssign(int değer) {
        writeln(__FUNCTION__);

        foreach (satır; satırlar) {
            satır[] = değer;
        }

        return this;
    }

    /* Belirtilen işleci ve değeri her elemana uygular ve
     * sonucu o elemana atar. */
    Matris opOpAssign(string işleç)(int değer) {
        writeln(__FUNCTION__);

        foreach (satır; satırlar) {
            mixin ("satır[] " ~ işleç ~ "= değer;");
        }

        return this;
    }

    /* Belirtilen boyutun uzunluğunu döndürür. */
    size_t opDollar(size_t boyut)() const
            if (boyut <= 1) {
        writeln(__FUNCTION__);

        static if (boyut == 0) {
            /* Sıfırıncı boyutun uzunluğu isteniyor;
             * 'satırlar' dizisinin uzunluğudur. */
            return satırlar.length;

        } else {
            /* Birinci boyutun uzunluğu isteniyor; 'satırlar'
             * dizisinin elemanlarının uzunluğudur. */
            return satırlar.length ? satırlar[0].length : 0;
        }
    }

    /* 'baş' ve 'son' ile belirlenen aralığı ifade eden bir
     * nesne döndürür.
     *
     * Not: Bu gerçekleştirmede 'boyut' parametresi
     * kullanılmıyor olsa da, bu bilgi başka bir tür için
     * yararlı olabilir. */
    Aralık opSlice(size_t boyut)(size_t baş, size_t son)
            if (boyut <= 1) {
        writeln(__FUNCTION__);

        return Aralık(baş, son);
    }

    /* Parametrelerle belirlenen alt matrisi döndürür. */
    Matris opIndex(A...)(A parametreler)
            if (A.length <= 2) {
        writeln(__FUNCTION__);

        /* Bütün elemanları temsil eden aralıklarla
         * başlıyoruz. Böylece opIndex'in parametresiz
         * kullanımında bütün elemanlar kapsanırlar. */
        Aralık[2] aralıklar = [ Aralık(0, opDollar!0),
                                Aralık(0, opDollar!1) ];

        foreach (boyut, p; parametreler) {
            static if (is (typeof(p) == Aralık)) {
                /* Bu boyut için 'matris[baş..son]' gibi
                 * aralık belirtilmiş; parametreyi olduğu gibi
                 * aralık olarak kullanabiliriz. */
                aralıklar[boyut] = p;

            } else static if (is (typeof(p) : size_t)) {
                /* Bu boyut için 'matris[i]' gibi tek konum
                 * değeri belirtilmiş; kullanmadan önce tek
                 * uzunluklu aralık oluşturmak gerekiyor. */
                aralıklar[boyut] = Aralık(p, p + 1);

            } else {
                /* Bu işlevin başka bir türle çağrılmasını
                 * beklemiyoruz. */
                static assert(
                    false, format("Geçersiz indeks türü: %s",
                                  typeof(p).stringof));
            }
        }

        /* 'parametreler'in karşılık geldiği alt matrisi
         * döndürüyoruz. */
        return altMatris(aralıklar[0], aralıklar[1]);
    }

    /* Belirtilen değeri belirtilen elemanlara atar. */
    Matris opIndexAssign(A...)(int değer, A parametreler)
            if (A.length <= 2) {
        writeln(__FUNCTION__);

        Matris altMatris = opIndex(parametreler);
        return altMatris = değer;
    }

    /* Belirtilen işleci ve değeri belirtilen elemanlara
     * uygular ve sonuçları yine aynı elemanlara atar. */
    Matris opIndexOpAssign(string işleç, A...)(int değer,
                                               A parametreler)
            if (A.length <= 2) {
        writeln(__FUNCTION__);

        Matris altMatris = opIndex(parametreler);
        mixin ("return altMatris " ~ işleç ~ "= değer;");
    }
}

/* Dizgi halinde belirtilen ifadeyi işletir ve hem işlemin
 * sonucunu hem de matrisin yeni durumunu yazdırır. */
void işlet(string ifade)(Matris m) {
    writefln("\n--- %s ---", ifade);
    mixin ("auto sonuç = " ~ ifade ~ ";");
    writefln("sonuç:\n%s", sonuç);
    writefln("m:\n%s", m);
}

void main() {
    enum yükseklik = 10;
    enum genişlik = 8;

    auto m = Matris(yükseklik, genişlik);

    int sayaç = 0;
    foreach (satır; 0 .. yükseklik) {
        foreach (sütun; 0 .. genişlik) {
            writefln("%s / %s ilkleniyor",
                     sayaç + 1, yükseklik * genişlik);

            m[satır, sütun] = sayaç;
            ++sayaç;
        }
    }

    writeln(m);

    işlet!("m[1, 1] = 42")(m);
    işlet!("m[0, 1 .. $] = 43")(m);
    işlet!("m[0 .. $, 3] = 44")(m);
    işlet!("m[$-4 .. $-1, $-4 .. $-1] = 7")(m);

    işlet!("m[1, 1] *= 2")(m);
    işlet!("m[0, 1 .. $] *= 4")(m);
    işlet!("m[0 .. $, 0] *= 10")(m);
    işlet!("m[$-4 .. $-2, $-4 .. $-2] -= 666")(m);

    işlet!("m[1, 1]")(m);
    işlet!("m[2, 0 .. $]")(m);
    işlet!("m[0 .. $, 2]")(m);
    işlet!("m[0 .. $ / 2, 0 .. $ / 2]")(m);

    işlet!("++m[1..3, 1..3]")(m);
    işlet!("--m[2..5, 2..5]")(m);

    işlet!("m[]")(m);
    işlet!("m[] = 20")(m);
    işlet!("m[] /= 4")(m);
    işlet!("(m[] += 5) /= 10")(m);
}
Özet

Önceki şablonlar bölümünün sonunda şunları hatırlatmıştım:

Bu bölümde de şu kavramları gördük: