D.ershane D Programlama Dili
Ali Çehreli

çıkarsama: [deduction], derleyicinin kendiliğinden anlaması
çöp toplayıcı: [garbage collector], işi biten nesneleri sonlandıran düzenek
doldurma baytı: [padding byte], değişkenleri hizalamak için aralarına gelen baytlar
evrensel: [global], evrensel isim alanında tanımlanmış, erişimi kısıtlanmamış
hizalama değeri: [alignment], bir türün değişkenlerinin bulunabileceği adres adımı
kapsam: [scope], küme parantezleriyle belirlenen bir alan
mikro işlemci: [CPU], bilgisayarın beyni
program yığıtı: [program stack], belleğin kısa ömürlü değişkenler için kullanılan bölgesi
sonlandırma: [destruct], nesneyi kullanımdan kaldırırken gereken işlemleri yapmak
statik: [static], derleme zamanında belirli olan
yaşam süreci: [object lifetime], bir değişkenin veya nesnenin tanımlanmasından işinin bitmesine kadar geçen süre
yazmaç: [register], mikro işlemcinin en temel iç depolama ve işlem birimi
... bütün sözlük



İngilizce Kaynaklar

Diğer



Bellek Yönetimi

Şimdiye kadar yazdığımız programlarda hiç bellek yönetimiyle ilgilenmek zorunda kalmadık çünkü D bellek yönetimi gerektirmeyen bir dildir. O yüzden, burada anlatılanlara büyük olasılıkla hiç ihtiyaç duymayacaksınız. Buna rağmen, D gibi sistem dillerinde alt düzey bellek işlemleri ile ilgilenmek gerekebilir.

Bellek yönetimi çok kapsamlı bir konudur. Bu bölümde yalnızca çöp toplayıcıyı tanıyacağız, çöp toplayıcıdan nasıl bellek ayrıldığını ve belirli bellek bölgelerine değişkenlerin nasıl yerleştirildiklerini göreceğiz.

Önceki bazı bölümlerde olduğu gibi, aşağıda kısaca yalnızca değişken yazdığım yerlerde yapı ve sınıf nesneleri de dahil olmak üzere her türden değişkeni kastediyorum.

Bellek

Bellek hem programın kendisini hem de kullandığı verileri barındırır. Bu yüzden diğer bilgisayar kaynaklarından daha önemlidir. Bu kaynak temelde işletim sistemine aittir. İşletim sistemi belleği ihtiyaçlar doğrultusunda programlara paylaştırır. Her programın kullanmakta olduğu bellek o programın belirli zamanlardaki ihtiyaçları doğrultusunda artabilir veya azalabilir. Belirli bir programın kullandığı bellek o program sonlandığında tekrar işletim sistemine geçer.

Bellek, değişken değerlerinin yazıldığı bir defter gibi düşünülebilir. Her değişken bellekte belirli bir yere yazılır. Her değişkenin değeri gerektikçe aynı yerden okunur ve kullanılır. Yaşamı sona eren değişkenlerin yerleri daha sonradan başka değişkenler için kullanılır.

Bellekle ilgili deneyler yaparken değişkenlerin adres değerlerini veren & işlecinden yararlanabiliriz:

import std.stdio;

void main()
{
    int i;
    int j;

    writeln("i: ", &i);
    writeln("j: ", &j);
}

Not: Adresler programın her çalıştırılışında büyük olasılıkla farklı olacaktır. Ek olarak, adres değerini edinmiş olmak, normalde bir mikro işlemci yazmacında yaşayacak olan bir değişkenin bile bellekte yaşamasına neden olur.

Çıktısı:

i: 7FFF2B633E28
j: 7FFF2B633E2C

Adreslerdeki tek fark olan son hanelere bakarak i'nin bellekte j'den hemen önce bulunduğunu görebiliyoruz: 8'e int'in büyüklüğü olan 4'ü eklersek on altılı sayı düzeninde C elde edilir.

Çöp toplayıcı

D programlarındaki dinamik değişkenler çöp toplayıcıya ait olan bellek bölgelerinde yaşarlar. Yaşamları sona eren değişkenler çöp toplayıcının işlettiği bir algoritma ile sonlandırılırlar. Bu değişkenlerin yerleri tekrar kullanılmak üzere geri alınır. Bu işleme aşağıda bazen çöp toplama, bazen de temizlik diyeceğim.

Çöp toplayıcının işlettiği algoritma çok kabaca şöyle açıklanabilir: Program yığıtı da dahil olmak üzere kök olarak adlandırılan bölgeler taranır. O bölgelerdeki değişkenler yoluyla doğrudan veya dolaylı olarak erişilebilen bütün bellek bölgeleri belirlenir ve program tarafından herhangi bir yolla erişilebilen bütün bölgelerin hâlâ kullanımda olduklarına karar verilir. Kullanımda olmadıkları görülen diğer bellek bölgelerindeki değişkenlerin sonlandırıcıları işletilir ve o bellek bölgeleri sonradan başka değişkenler için kullanılmak üzere geri alınır.

Bazı çöp toplayıcılar kullanımda olan bütün değişkenleri bellekte yan yana dursunlar diye başka yerlere taşıyabilirler. Programın tutarlılığı bozulmasın diye de o değişkenleri gösteren bütün göstergelerin değerlerini otomatik olarak değiştirirler.

Temizlik işlemlerinin hangi sırada işletildikleri belirsizdir. Örneğin, nesnelerin referans türündeki (göstergeler dahil) üyeleri kendilerini barındıran nesneden daha önce sonlanmış olabilirler. Bu yüzden, yaşamları çöp toplayıcıya ait olan ve kendileri referans türünden olan üyelerin sonlandırıcı işlevler içinde kullanılmaları hatalıdır. Bu kavram sonlanma sıralarının tam olarak belirli olduğu C++ gibi bazı dillerden farklıdır.

Temizlik işlemleri boş yerin azalmaya başlaması gibi nedenlerle ve önceden kestirilemeyecek zamanlarda işletilebilir. Temizlik işlemleri devam ederken yeni yer ayrılması çöp toplama düzeneğinde karışıklık yaratabileceğinden programa ait olan bütün iş parçacıkları temizlik sırasında kısa süreliğine durdurulabilirler. Bu işlem sırasında programın tutukluk yaptığı hissedilebilir.

Programcının çöp toplayıcının işine karışması çoğu durumda gerekmese de temizlik işlemlerinin hemen işletilmeleri veya ertelenmeleri gibi bazı işlemler core.memory modülünün olanakları ile sağlanabilir.

Temizlik başlatmak ve ertelemek

Programın tutukluk yapmadan çalışması gereken yerlerde temizlik işlemlerinin ertelenmesi mümkündür. GC.disable temizlik işlemlerini erteler, GC.enable da tekrar etkinleştirir:

    GC.disable();

// ... tutukluk hissedilmeden işlemesi gereken işlemler ...

    GC.enable();

Ancak, temizlik işlemlerinin kesinlikle işletilmeyecekleri garantili değildir: Çöp toplayıcı belleğin çok azaldığını farkettiği durumlarda boş yer bulmak için yine de işletebilir.

Temizlik işlemleri programın tutukluk yapmasının sorun oluşturmadığının bilindiği bir zamanda programcı tarafından GC.collect() ile başlatılabilir:

import core.memory;

// ...

    GC.collect();        // temizlik başlatır

Normalde, çöp toplayıcı boş kalan bellek bölgelerini işletim sistemine geri vermez ve ileride oluşturulacak olan değişkenler için elinde tutmaya devam eder. Bunun bir sorun oluşturduğunun bilindiği programlarda boş bellek bölgeleri GC.minimize() ile işletim sistemine geri verilebilir:

    GC.minimize();
Bellekten yer ayırmak

Bellekten herhangi bir amaç için bellek bölgesi ayrılabilir. Böyle bir bölge örneğin üzerinde değişkenler kurmak için kullanılabilir.

Belirli sayıda bayttan oluşan bir bellek bölgesi sabit uzunluklu bir dizi olarak ayrılabilir:

    ubyte[100] yer;                     // 100 baytlık yer

Yukarıdaki dizi 100 baytlık bellek bölgesi olarak kullanılmaya hazırdır. Bazen bu bölgenin uybte gibi bir türle ilgisi olması yerine hiçbir türden olması istenebilir. Bunun için eleman türü olarak void seçilir ve void türü herhangi bir değer alamadığından böyle dizilerin özel olarak =void ile ilklenmeleri gerekir:

    void[100] yer = void;               // 100 baytlık yer

Bu bölümde bellek ayırmak için yalnızca core.memory modülündeki GC.calloc işlevini kullanacağız. Aynı modüldeki diğer bellek ayırma işlevlerini kendiniz araştırmak isteyebilirsiniz. Ek olarak, C standart kütüphanesinin olanaklarını içeren std.c.stdlib modülündeki calloc() ve diğer işlevler de kullanılabilir.

GC.calloc bellekten kaç bayt istendiğini parametre olarak alır ve ayırdığı bellek bölgesinin başlangıç adresini döndürür:

import core.memory;
// ...
    void * yer = GC.calloc(100);        // 100 baytlık yer

void* ile gösterilen bir bölgenin hangi tür için kullanılacağı o türün göstergesine dönüştürülerek belirlenebilir:

    int * intYeri = cast(int*)yer;

Ancak, o ara adım çoğunlukla atlanır ve GC.calloc'un döndürdüğü adres istenen türe doğrudan dönüştürülür:

    int * intYeri = cast(int*)GC.calloc(100);

Öylesine seçmiş olduğum 100 gibi hazır değerler kullanmak yerine örneğin türün uzunluğu ile nesne adedi çarpılabilir:

    // 25 int için yer
    int * yer = cast(int*)GC.calloc(int.sizeof * 25);

Sınıf nesnelerinin uzunluğu konusunda önemli bir fark vardır: .sizeof sınıf nesnesinin değil, sınıf değişkeninin uzunluğudur. Sınıf nesnesinin uzunluğu __traits(classInstanceSize) ile öğrenilir:

    // 10 Sınıf nesnesi için yer
    Sınıf * yer =
        cast(Sınıf*)GC.calloc(__traits(classInstanceSize, Sınıf) * 10);

İstenen büyüklükte bellek ayrılamadığı zaman core.exception.OutOfMemoryError türünde bir hata atılır:

    void * yer = GC.calloc(10_000_000_000);

O kadar bellek ayrılamayan durumdaki çıktısı:

core.exception.OutOfMemoryError

Ayrılan bellek işi bittiğinde GC.free ile geri verilebilir:

    GC.free(yer);

Ancak, açıkça çağrılan free() sonlandırıcıları işletmez. Sonlanmaları gereken nesnelerin bellek geri verilmeden önce destroy() ile teker teker sonlandırılmaları gerekir.

Daha önce çöp toplayıcıdan alınmış olan bir bellek bölgesinin uzatılması mümkündür. GC.realloc(), daha önce edinilmiş olan adres değerini ve istenen yeni uzunluğu parametre olarak alır ve yeni uzunlukta bir yer döndürür. Aşağıdaki kod önceden 100 bayt olarak ayrılmış olan bellek bölgesini 200 bayta uzatıyor:

    void * eskiYer = GC.calloc(100);
// ...
    void * yeniYer = GC.realloc(eskiYer, 200);

realloc() gerçekten gerekmedikçe yeni yer ayırmaz:

GC.realloc C kütüphanesindeki aynı isimli işlevden gelmiştir. Görevi hem fazla çeşitli hem de fazla karmaşık olduğundan hatalı tasarlanmış bir işlev olarak kabul edilir. GC.realloc'un şaşırtıcı özelliklerinden birisi, asıl bellek GC.calloc ile ayrılmış bile olsa uzatılan bölümün sıfırlanmamasıdır. Bu yüzden, belleğin sıfırlanmasının önemli olduğu durumlarda aşağıdaki gibi bir işlevden yararlanılabilir (bellekNitelikleri parametresinin anlamını biraz aşağıda göreceğiz):

import core.memory;

/* GC.realloc gibi işler. Ondan farklı olarak, belleğin
 * uzatıldığı durumda eklenen baytları sıfırlar. */
void * boşOlarakUzat(void * yer,
                     size_t eskiUzunluk,
                     size_t yeniUzunluk,
                     GC.BlkAttr bellekNitelikleri = GC.BlkAttr.NONE,
                     const TypeInfo türBilgisi = null)
{
    /* Asıl işi GC.realloc'a yaptırıyoruz. */
    yer = GC.realloc(yer, yeniUzunluk,
                     bellekNitelikleri, türBilgisi);

    /* Eğer varsa, yeni eklenen bölümü sıfırlıyoruz. */
    if (yeniUzunluk > eskiUzunluk) {
        import std.c.string;

        auto eklenenYer = yer + eskiUzunluk;
        auto eklenenUzunluk = yeniUzunluk - eskiUzunluk;

        memset(eklenenYer, 0, eklenenUzunluk);
    }

    return yer;
}

std.c.string modülünde tanımlı olan memset() belirtilen adresteki belirtilen sayıdaki bayta belirtilen değeri atar. Örneğin, yukarıdaki çağrı eklenenYer'deki eklenenUzunluk adet baytı 0 yapar.

boşOlarakUzat() işlevini aşağıdaki bir örnekte kullanacağız.

(Not: Yukarıdaki import bildirimlerinden birisinin iç kapsamda kullanılabildiğine dikkat edin. Bu, D'ye bu kitabın yazılmaya başlanmasından daha sonra eklenen bir olanaktır.)

GC.realloc ile benzer amaçla kullanılan GC.extend'in davranışı çok daha basittir çünkü yalnızca yukarıdaki ilk maddeyi uygular: Eski yerin hemen sonrası yeni uzunluğu karşılayamıyorsa hiçbir işlem yapmaz ve bu durumu 0 döndürerek bildirir.

Ayrılan belleğin temizlik işlemlerinin belirlenmesi

Çöp toplayıcı algoritmasında geçen kavramlar ve adımlar bir enum türü olan BlkAttr'ın değerleri ile her bellek bölgesi için ayrı ayrı ayarlanabilir. BlkAttr, GC.calloc ve diğer bellek ayırma işlevlerine parametre olarak gönderilebilir ve bellek bölgelerinin niteliklerini belirlemek için kullanılır. BlkAttr türünün değerleri şunlardır:

Bu değerler Bit İşlemleri bölümünde gördüğümüz işleçlerle birlikte kullanılabilecek biçimde seçilmişlerdir. Örneğin, iki değer | işleci ile aşağıdaki gibi birleştirilebilir:

    const bellekAyarları = GC.BlkAttr.NO_SCAN | GC.BlkAttr.APPENDABLE;

Doğal olarak, çöp toplayıcı yalnızca kendi ayırdığı bellek bölgelerini tanır ve temizlik işlemleri sırasında yalnızca o bölgeleri tarar. Örneğin, std.c.stdlib.calloc ile ayrılmış olan bellek bölgelerinden çöp toplayıcının normalde haberi olmaz.

Kendisinden alınmamış olan bir bölgenin çöp toplayıcının yönetimine geçirilmesi için GC.addRange() işlevi kullanılır. Bunun karşıtı olarak, bellek geri verilmeden önce de GC.removeRange()'in çağrılması gerekir.

Bazı durumlarda çöp toplayıcı kendisinden ayrılmış olan bir bölgeyi gösteren hiçbir referans bulamayabilir. Örneğin, ayrılan belleğin tek referansı bir C kütüphanesi içinde tutuluyor olabilir. Böyle bir durumda çöp toplayıcı o bölgenin kullanımda olmadığını düşünecektir.

GC.addRoot(), belirli bir bölgeyi çöp toplayıcıya tanıtır ve oradan dolaylı olarak erişilebilen bütün nesneleri de yönetmesini sağlar. Bunun karşıtı olarak, bellek geri verilmeden önce de GC.removeRoot() işlevinin çağrılması gerekir.

Bellek uzatma örneği

realloc()'un kullanımını göstermek için dizi gibi işleyen çok basit bir yapı tasarlayalım. Çok kısıtlı olan bu yapıda yalnızca eleman ekleme ve elemana erişme olanakları bulunsun. D dizilerinde olduğu gibi bu yapının da kapasitesi olsun. Aşağıdaki yapı kapasiteyi gerektikçe yukarıda tanımladığımız ve kendisi GC.realloc'tan yararlanan boşOlarakUzat() ile arttırıyor:

struct Dizi(T)
{
    T * yer;          // Elemanların bulunduğu yer
    size_t kapasite;  // Toplam kaç elemanlık yer olduğu
    size_t uzunluk;   // Eklenmiş olan eleman adedi

    /* Belirtilen numaralı elemanı döndürür */
    T eleman(size_t numara)
    {
        import std.string;
        enforce(numara < uzunluk,
                format("%s numara yasal değil", numara));

        return *(yer + numara);
    }

    /* Elemanı dizinin sonuna ekler */
    void ekle(T eleman)
    {
        writefln("%s numaralı eleman ekleniyor", uzunluk);

        if (uzunluk == kapasite) {
            /* Yeni eleman için yer yok; kapasiteyi arttırmak
             * gerekiyor. */
            size_t yeniKapasite = kapasite + (kapasite / 2) + 1;
            kapasiteArttır(yeniKapasite);
        }

        /* Elemanı en sona yerleştiriyoruz */
        *(yer + uzunluk) = eleman;
        ++uzunluk;
    }

    void kapasiteArttır(size_t yeniKapasite)
    {
        writefln("Kapasite artıyor: %s -> %s",
                 kapasite, yeniKapasite);

        auto eskiUzunluk = kapasite * T.sizeof;
        auto yeniUzunluk = yeniKapasite * T.sizeof;

        /* Bu bölgeye yerleştirilen bayt değerlerinin
         * tesadüfen başka değişkenlerin göstergeleri
         * sanılmalarını önlemek için NO_SCAN belirtecini
         * kullanıyoruz. */
        yer = cast(T*)boşOlarakUzat(
            yer, eskiUzunluk, yeniUzunluk, GC.BlkAttr.NO_SCAN);

        kapasite = yeniKapasite;
    }
}

Bu dizinin kapasitesi her seferinde yaklaşık olarak %50 oranında arttırılıyor. Örneğin, 100 elemanlık yer tükendiğinde yeni kapasite 151 oluyor. (Yeni kapasite hesaplanırken eklenen 1 değeri, başlangıç durumunda sıfır olan kapasite için özel bir işlem gerekmesini önlemek içindir. Öyle olmasaydı, sıfırın %50 fazlası da sıfır olacağından kapasite hiç artamazdı.)

Bu yapıyı double türünde elemanlarla şöyle deneyebiliriz:

import std.stdio;
import core.memory;
import std.exception;

// ...

void main()
{
    auto dizi = Dizi!double();

    size_t adet = 10;

    foreach (i; 0 .. adet) {
        double elemanDeğeri = i * 1.1;
        dizi.ekle(elemanDeğeri);
    }

    writeln("Bütün elemanlar:");

    foreach (i; 0 .. adet) {
        write(dizi.eleman(i), ' ');
    }

    writeln();
}

Çıktısı:

0 numaralı eleman ekleniyor
Kapasite artıyor: 0 -> 1
1 numaralı eleman ekleniyor
Kapasite artıyor: 1 -> 2
2 numaralı eleman ekleniyor
Kapasite artıyor: 2 -> 4
3 numaralı eleman ekleniyor
4 numaralı eleman ekleniyor
Kapasite artıyor: 4 -> 7
5 numaralı eleman ekleniyor
6 numaralı eleman ekleniyor
7 numaralı eleman ekleniyor
Kapasite artıyor: 7 -> 11
8 numaralı eleman ekleniyor
9 numaralı eleman ekleniyor
Bütün elemanlar:
0 1.1 2.2 3.3 4.4 5.5 6.6 7.7 8.8 9.9 
Değişkenleri belirli bir yerde kurmak

new ifadesi iki işlem gerçekleştirir:

  1. Bellekten nesnenin sığacağı kadar yer ayırır. Bu bellek bölgesi henüz hiçbir nesneyle ilgili değildir.
  2. Nesnenin kurucu işlevini o bellek bölgesi üzerinde işletir. Nesne ancak bu işlemden sonra o bölgeye yerleştirilmiş olur.

Bu işlemlerden birincisinin GC.calloc ve başka işlevlerle gerçekleştirilebildiğini yukarıda gördük. Bir sistem dili olan D, normalde otomatik olarak işletilen ikinci adımın da programcı tarafından belirlenmesine olanak verir.

Nesnelerin belirli bir adreste kurulması için "yerleştir" anlamına gelen std.conv.emplace kullanılır.

emplace()'in kullanımına geçmeden önce türlerin hizalama birimi kavramını anlamak gerekir.

Türlerin .alignof niteliği

Değişkenlerin kurulabilecekleri adres değerleri ile ilgili bir kısıtlama vardır: Her tür değişken ancak belirli bir değerin katı olan adreslerde bulunabilir. Bu değere o türün hizalama birimi denir. Örneğin, int değişkenler ancak dördün katı olan adreslerde (4, 8, 12, vs.) bulunabilirler. Bunun nedeni, int türünün hizalama biriminin 4 olmasıdır.

Türlerin .alignof niteliği hizalama birimini döndürür. Ancak, sınıflarda .alignof sınıf nesnesinin değil, sınıf değişkeninin hizalama birimidir. Sınıf nesnesinin hizalama birimi için std.traits.classInstanceAlignment kullanılmalıdır.

Aşağıdaki program çeşitli türün hizalama birimini yazdırıyor.

import std.stdio;
import std.typetuple;
import std.traits;

struct BoşYapı
{}

struct Yapı
{
    char c;
    double d;
}

class BoşSınıf
{}

class Sınıf
{
    char karakter;
}

void main()
{
    alias Türler = TypeTuple!(char, short, int, long,
                              double, real,
                              string, int[int], int*,
                              BoşYapı, Yapı, BoşSınıf, Sınıf);

    writeln(" Uzunluk  Hizalama  Tür\n",
            "========================");

    foreach (Tür; Türler) {
        static if (is (Tür == class)) {
            size_t uzunluk = __traits(classInstanceSize, Tür);
            size_t hizalama = classInstanceAlignment!Tür;

        } else {
            size_t uzunluk = Tür.sizeof;
            size_t hizalama = Tür.alignof;
        }

        writefln("%6s%9s     %s",
                 uzunluk, hizalama, Tür.stringof);
    }
}

Bu programın çıktısı farklı ortamlarda farklı olabilir:

 Uzunluk  Hizalama  Tür
========================
     1        1     char
     2        2     short
     4        4     int
     8        8     long
     8        8     double
    16       16     real
    16        8     string
     8        8     int[int]
     8        8     int*
     1        1     BoşYapı
    16        8     Yapı
    16        8     BoşSınıf
    17        8     Sınıf

Nesneleri belirli adreslerde kurarken hizalama birimlerine uymak gerekir.

Bunun örneğini görmek için yukarıdaki 17 bayt uzunluğundaki Sınıf türünün iki nesnesinin bellekte yan yana nasıl durabileceklerine bakalım. Her ne kadar yasal bir adres olmasa da, örneği kolaylaştırmak için birinci nesnenin 0 adresinde bulunduğunu varsayalım. Bu nesneyi oluşturan baytlar 0'dan 16'ya kadar olan adreslerdedir:

     0    1           16
  +----+----+- ... -+----+- ...
  | <- birinci nesne --> |
  +----+----+- ... -+----+- ...

Bir sonraki boş yerin adresi 17 olduğu halde, o adres değeri Sınıf'ın hizalama birimi olan 8'in katı olmadığından ikinci nesne orada kurulamaz. İkinci nesnenin 8'in katı olan bir sonraki adrese, yani 24 adresine yerleştirilmesi gerekir. Aradaki kullanılmayan baytlara doldurma baytları denir:

     0    1           16   17           23   24   25           30
  +----+----+- ... -+----+----+- ... -+----+----+----+- ... -+----+- ...
  | <- birinci nesne --> | <- doldurma --> | <-- ikinci nesne --> |
  +----+----+- ... -+----+----+- ... -+----+----+----+- ... -+----+- ...

Bir nesnenin belirli bir aday adresten sonra yasal olarak kurulabileceği ilk adresi elde etmek için şu hesap kullanılabilir:

    (adayAdres + hizalamaBirimi - 1)
    / hizalamaBirimi
    * hizalamaBirimi

Yukarıdaki hesabın doğru olarak işlemesi için bölme işleminden kalanın gözardı edilmesi şarttır. O yüzden o hesapta tamsayı türleri kullanılır.

Aşağıda emplace()'in örneklerini gösterirken yukarıdaki hesabı uygulayan şu işlevden yararlanacağız:

T * hizalıAdres(T)(T * adayAdres)
{
    import std.traits;

    static if (is (T == class)) {
        const hizalama = classInstanceAlignment!T;

    } else {
        const hizalama = T.alignof;
    }

    const sonuç = (cast(size_t)adayAdres + hizalama - 1)
                  / hizalama * hizalama;
    return cast(T*)sonuç;
}

Yukarıdaki işlev nesnenin türünü şablon parametresinden otomatik olarak çıkarsıyor. Onun void* adresleri ile işleyen yüklemesini de şöyle yazabiliriz:

void * hizalıAdres(T)(void * adayAdres)
{
    return hizalıAdres(cast(T*)adayAdres);
}

Bu işlev, aşağıda emplace() ile sınıf nesneleri oluştururken yararlı olacak.

Son olarak, yukarıdaki işlevden yararlanan yardımcı bir işlev daha tanımlayalım. Bu işlev, nesnenin boşluklarla birlikte kaç bayt yer tuttuğunu döndürür:

size_t boşlukluUzunluk(T)()
{
    static if (is (T == class)) {
        size_t uzunluk = __traits(classInstanceSize, T);

    } else {
        size_t uzunluk = T.sizeof;
    }

    return cast(size_t)hizalıAdres(cast(T*)uzunluk);
}
Yapı nesnelerini belirli bir yerde kurmak

emplace(), nesnenin kurulacağı adresi parametre olarak alır ve o adreste bir nesne kurar. Eğer varsa, nesnenin kurucu işlevinin parametreleri bu adresten sonra bildirilir:

import std.conv;
// ...
    emplace(adres, /* ... kurucu parametreleri ... */);

Yapı nesneleri kurarken türün ayrıca belirtilmesi gerekmez; emplace() hangi türden nesne kuracağını kendisine verilen göstergenin türünden anlar. Örneğin, aşağıdaki emplace() çağrısında öğrenciAdresi'nin türü bir Öğrenci* olduğundan emplace() o adreste bir Öğrenci nesnesi kurar:

        Öğrenci * öğrenciAdresi = hizalıAdres(adayAdres);
// ...
        emplace(öğrenciAdresi, isim, numara);

Yukarıdaki işlevlerden yararlanan aşağıdaki program bütün nesneleri alabilecek büyüklükte bir bölge ayırıyor ve nesneleri o bölge içindeki hizalı adreslerde kuruyor:

import std.stdio;
import std.string;
import core.memory;
import std.conv;

// ...

struct Öğrenci
{
    string isim;
    int numara;

    string toString()
    {
        return format("%s(%s)", isim, numara);
    }
}

void main()
{
    /* Önce bu türle ilgili bilgi yazdırıyoruz. */
    writefln("Öğrenci.sizeof: %#x (%s) bayt",
             Öğrenci.sizeof, Öğrenci.sizeof);
    writefln("Öğrenci.alignof: %#x (%s) bayt",
             Öğrenci.alignof, Öğrenci.alignof);

    string[] isimler = [ "Deniz", "Pınar", "Irmak" ];
    auto toplamBayt =
        boşlukluUzunluk!Öğrenci() * isimler.length;

    /*
     * Bütün Öğrenci nesnelerine yetecek kadar yer ayırıyoruz.
     *
     * UYARI! Bu dilimin eriştirdiği nesneler henüz
     * kurulmamışlardır.
     */
    Öğrenci[] öğrenciler =
        (cast(Öğrenci*)GC.calloc(toplamBayt))[0 .. isimler.length];

    foreach (int i, isim; isimler) {
        Öğrenci * adayAdres = öğrenciler.ptr + i;
        Öğrenci * öğrenciAdresi = hizalıAdres(adayAdres);
        writefln("adres %s: %s", i, öğrenciAdresi);

        auto numara = 100 + i;
        emplace(öğrenciAdresi, isim, numara);
    }

    /* Bütün elemanları kurulmuş olduğundan bir Öğrenci dilimi
     * olarak kullanmakta artık bir sakınca yoktur. */
    writeln(öğrenciler);
}

Yukarıdaki program Öğrenci türünün uzunluğunu, hizalama birimini, ve her öğrencinin kurulduğu adresi de yazdırıyor:

Öğrenci.sizeof: 0x18 (24) bayt
Öğrenci.alignof: 0x8 (8) bayt
adres 0: 7FCF0B0F2F00
adres 1: 7FCF0B0F2F18
adres 2: 7FCF0B0F2F30
[Deniz(100), Pınar(101), Irmak(102)]
Sınıf nesnelerini belirli bir yerde kurmak

Sınıf değişkenlerinin nesnenin tam türünden olması gerekmez. Örneğin, Hayvan değişkenleri Kedi nesnelerine de erişim sağlayabilirler. Bu yüzden emplace(), kuracağı nesnenin türünü kendisine verilen göstergenin türünden anlayamaz ve asıl türün emplace()'e şablon parametresi olarak bildirilmesini gerektirir. (Not: Ek olarak, sınıf göstergesi nesnenin değil, değişkenin adresi olduğundan türün açıkça belirtilmesi nesne mi yoksa değişken mi yerleştirileceği seçimini de programcıya bırakmış olur.)

Sınıf nesnelerinin kurulacağı yer void[] türünde bir dilim olarak belirtilir. Bunlara göre sınıf nesneleri kurarken şu söz dizimi kullanılır:

    Tür değişken =
        emplace!Tür(voidDilimi,
                         /* ... kurucu parametreleri ... */);

emplace(), belirtilen yerde bir nesne kurar ve o nesneye erişim sağlayan bir sınıf değişkeni döndürür.

Bunları denemek için bir Hayvan sıradüzeninden yararlanalım. Bu sıradüzene ait olan nesneleri GC.calloc ile ayrılmış olan bir belleğe yan yana yerleştireceğiz. Alt sınıfları özellikle farklı uzunlukta seçerek her nesnenin yerinin bir öncekinin uzunluğuna bağlı olarak nasıl hesaplanabileceğini göreceğiz.

interface Hayvan
{
    string şarkıSöyle();
}

class Kedi : Hayvan
{
    string şarkıSöyle()
    {
        return "miyav";
    }
}

class Papağan : Hayvan
{
    string[] sözler;

    this(string[] sözler)
    {
        this.sözler = sözler;
    }

    string şarkıSöyle()
    {
        /* std.algorithm.joiner, belirtilen aralıktaki
         * elemanları belirtilen ayraçla birleştirir. */
        return sözler.joiner(", ").to!string;
    }
}

Nesnelerin yerleştirilecekleri bölgeyi GC.calloc ile ayıracağız ve bu bölgedeki nesnelerin sonlandırıcılarının işletilmelerine engel olmamak için de FINALIZE niteliğini kullanacağız:

    auto kapasite = 10_000;
    void * boşYer = GC.calloc(kapasite, GC.BlkAttr.FINALIZE);

Normalde, nesneler kuruldukça o bölgenin tükenmediğinden de emin olunması gerekir. Örneği kısa tutmak için bu konuyu gözardı edelim ve kurulacak olan iki nesnenin on bin bayta sığacaklarını varsayalım.

O bölgede önce bir Kedi nesnesi sonra da bir Papağan nesnesi kuracağız:

    Kedi kedi = emplace!Kedi(kediYeri);
// ...
    Papağan papağan =
        emplace!Papağan(papağanYeri, [ "merrba", "aloo" ]);

Dikkat ederseniz Papağan'ın kurucusunun gerektirdiği parametreler nesnenin yerinden sonra belirtiliyorlar.

emplace() çağrılarının döndürdükleri değişkenler bir Hayvan dizisine eklenecekler ve daha sonra bir foreach döngüsünde kullanılacaklar:

    Hayvan[] hayvanlar;
// ...
    hayvanlar ~= kedi;
// ...
    hayvanlar ~= papağan;

    foreach (hayvan; hayvanlar) {
        writeln(hayvan.şarkıSöyle());
    }

Diğer açıklamaları programın içine yazıyorum:

import std.stdio;
import std.algorithm;
import std.conv;
import core.memory;

// ...

void main()
{
    /* Bu bir Hayvan değişkeni dizisidir; Hayvan nesnesi
     * dizisi değildir. */
    Hayvan[] hayvanlar;

    /* On bin baytın bu örnekte yeterli olduğunu varsayalım.
     * Normalde nesnelerin buraya gerçekten sığacaklarının da
     * denetlenmesi gerekir. */
    auto kapasite = 10_000;
    void * boşYer = GC.calloc(kapasite, GC.BlkAttr.FINALIZE);

    /* İlk önce bir Kedi nesnesi yerleştireceğiz. */
    void * kediAdayAdresi = boşYer;
    void * kediAdresi = hizalıAdres!Kedi(kediAdayAdresi);
    writeln("Kedi adresi   : ", kediAdresi);

    /* Sınıflarda emplace()'e void[] verildiğinden adresten
     * dilim elde etmek gerekiyor. */
    size_t kediUzunluğu = __traits(classInstanceSize, Kedi);
    void[] kediYeri = kediAdresi[0..kediUzunluğu];

    /* Kedi'yi o yerde kuruyoruz ve döndürülen değişkeni
     * diziye ekliyoruz. */
    Kedi kedi = emplace!Kedi(kediYeri);
    hayvanlar ~= kedi;

    /* Papağan'ı Kedi nesnesinden sonraki ilk uygun adreste
     * kuracağız. */
    void * papağanAdayAdresi = kediAdresi + kediUzunluğu;
    void * papağanAdresi =
        hizalıAdres!Papağan(papağanAdayAdresi);
    writeln("Papağan adresi: ", papağanAdresi);

    size_t papağanUzunluğu =
        __traits(classInstanceSize, Papağan);
    void[] papağanYeri = papağanAdresi[0..papağanUzunluğu];

    Papağan papağan =
        emplace!Papağan(papağanYeri, [ "merrba", "aloo" ]);
    hayvanlar ~= papağan;

    /* Nesneleri kullanıyoruz. */
    foreach (hayvan; hayvanlar) {
        writeln(hayvan.şarkıSöyle());
    }
}

Çıktısı:

Kedi adresi   : 7F869469E000
Papağan adresi: 7F869469E018
miyav
merrba, aloo

Programın adımlarını açıkça gösterebilmek için bütün işlemleri main içinde ve belirli türlere bağlı olarak yazdım. O işlemlerin iyi yazılmış bir programda yeniNesne(T) gibi bir şablon içinde bulunmalarını bekleriz.

Nesneyi belirli bir zamanda sonlandırmak

new işlecinin tersi, sonlandırıcı işlevin işletilmesi ve nesne için ayrılmış olan belleğin çöp toplayıcı tarafından geri alınmasıdır. Bu işlemler normalde belirsiz bir zamanda otomatik olarak işletilir.

Bazı durumlarda sonlandırıcı işlevin programcının istediği bir zamanda işletilmesi gerekebilir. Örneğin, açmış olduğu bir dosyayı sonlandırıcı işlevinde kapatan bir nesnenin sonlandırıcısının hemen işletilmesi gerekebilir.

Buradaki kullanımında "ortadan kaldır" anlamına gelen destroy(), nesnenin sonlandırıcı işlevinin hemen işletilmesini sağlar:

    destroy(değişken);

destroy() değişkene türünün .init değerini atar. Sınıf değişkenlerinin ilk değeri null olduğundan nesne o noktadan sonra kullanılamaz. destroy() yalnızca sonlandırıcı işlevi işletir; belleğin gerçekten ne zaman geri verileceği yine de çöp toplayıcının kararına kalmıştır.

Nesneyi çalışma zamanında ismiyle kurmak

Object sınıfının factory() isimli üye işlevi türün ismini parametre olarak alır, o türden bir nesne kurar, ve adresini döndürür. factory(), türün kurucusu için parametre almaz; bu yüzden türün parametresiz olarak kurulabilmesi şarttır:

module deneme;

import std.stdio;

interface Hayvan
{
    string ses();
}

class Kedi : Hayvan
{
    string ses()
    {
        return "miyav";
    }
}

class Köpek : Hayvan
{
    string ses()
    {
        return "hav";
    }
}

void main()
{
    string[] kurulacaklar = [ "Kedi", "Köpek", "Kedi" ];

    Hayvan[] hayvanlar;

    foreach (türİsmi; kurulacaklar) {
        /* "Sözde değişken" __MODULE__, her zaman için içinde
         * bulunulan modülün ismidir ve bir string olarak
         * derleme zamanında kullanılabilir. */
        const tamİsim = __MODULE__ ~ '.' ~ türİsmi;
        writefln("%s kuruluyor", tamİsim);
        hayvanlar ~= cast(Hayvan)Object.factory(tamİsim);
    }

    foreach (hayvan; hayvanlar) {
        writeln(hayvan.ses());
    }
}

O programda hiç new kullanılmadığı halde üç adet Hayvan nesnesi oluşturulmuş ve hayvanlar dizisine eklenmiştir:

deneme.Kedi kuruluyor
deneme.Köpek kuruluyor
deneme.Kedi kuruluyor
miyav
hav
miyav

Object.factory()'ye türün tam isminin verilmesi gerekir. O yüzden yukarıdaki tür isimleri "Kedi" ve "Köpek" gibi kısa olarak değil, modülün ismi ile birlikte "deneme.Kedi" ve "deneme.Köpek" olarak belirtiliyorlar.

factory'nin dönüş türü Object'tir; bu türün yukarıdaki cast(Hayvan) kullanımında gördüğümüz gibi doğru türe açıkça dönüştürülmesi gerekir.

Özet