July 17, 2010

TDPL'i okurken rastladığım önemli konuları yazmaya devam ediyorum.

Sınıfların referans, yapıların değer türü olduklarını biliyoruz.

Buna bağlı olarak, sınıflarda atama işleci (opAssign) programcı tarafından yazılamaz; çünkü iki sınıf değişkeninin atanmaları, artık soldakinin de sağdakinin eriştirdiğine erişim sağlamasına neden olur. Bir değer ataması söz konusu değildir.

Yine buna bağlı olarak, yapılarda da atama işlemi otomatik olarak bütün üyeleri sıra ile atar. Buraya kadar çok güzel...

Yapının referans üyesi bulunduğunda ise işler karışmaya başlar; çünkü üyelerin sıra ile atanmaları, referans üyelerin atama kuralları gereği o üyelerin aynı nesneyi erişim sağlamalarına ve onu paylaşmalarına neden olur. Örneğin iki yapının dizi üyeleri, aynı dizi elemanlarına erişim sağlamaya başlarlar:

import std.stdio;

struct Yapı
{
   int[] dizi;

   this(int uzunluk)
   {
       dizi.length = uzunluk;
   }
}

void main()
{
   auto y1 = Yapı(10);
   auto y2 = Yapı(20);

   // ... daha sonra ...
   y1 = y2;

   y1.dizi[0] = 11;            // y1'in dizi elemanı değiştiriliyor

   assert(y2.dizi[0] == 11);   // ama y2'ninki de değişmiş
}

O kodda farklı iki yapı nesnesi kuruluyor ve sonra biri ötekine atanıyor. Bunun sonucunda, y1'in dizi üyesinin elemanı değiştirilince, y2'ninkinin de değiştiğini görüyoruz. Çünkü artık iki nesnenin dizi üyeleri aynı dizi elemanlarına erişim sağlıyorlar.

Eğer bu istenen bir şey değilse, ve nesnelerin kendilerine ait dizi elemanları olması isteniyorsa, o zaman opAssign işleci tanımlanır.

Yukarıdakilere D.ershane'nin "Yapılar" dersinde "Referans türünden olan üyelere dikkat!" başlığı altında anlatmıştım:

http://ddili.org/ders/d/yapilar.html

'opAssign' işlecini de "Kurucu ve Diğer Özel İşlevler" dersinde "Atama işleci" başlığı altında anlatmıştım:

http://ddili.org/ders/d/ozel_islevler.html

İşte, eğer her yapı nesnesinin kendisine ait dizisinin olmasını istiyorsak, opAssign'ı şöyle tanımlayabiliyoruz:

import std.stdio;

struct Yapı
{
   int[] dizi;

   this(int uzunluk)
   {
       dizi.length = uzunluk;
   }

   ref Yapı opAssign(const ref Yapı sağdaki)
   {
       // sağdakinin kopyasına eşitliyoruz
       dizi = sağdaki.dizi.dup;
       return this;
   }
}

void main()
{
   auto y1 = Yapı(10);
   auto y2 = Yapı(20);

   // ... daha sonra ...
   y1 = y2;

   y1.dizi[0] = 11;            // y1'in dizi elemanı değiştiriliyor

   assert(y2.dizi[0] == 0);   // artık y2'ninkinin bundan haberi yok
}

Atama işleci içinde sağdakinin dizisinin kopyasını aldığımız için bu sefer, her ikisinin dizi elemanları farklı oluyor.

Çok hızlıca iki noktayı daha hatırlatayım:

  1. Dönüş türünün 'ref' olmasının nedeni, yapıların zincirleme olarak atanmaları sırasında işe yarıyor:
   y1 = y2 = y3;

Dönüş türünde ref olmadığında o satır derlenemiyor.

Bir başka nedeni de performans: Atamanın sonucunu hemen kullanmak isteyen bir ifadede dönüş değeri kopyalanmamış oluyor:

   writeln(y1 = y2);

Atamadan hemen sonra y1 writeln tarafından kullanılıyor. Dönüş türü ref olduğu için, y1 writeln'e kopyalanmıyor. Böylece daha hızlı...

  1. Parametrenin türünün ref olması da performans nedeniyle: Yalnızca '(Yapı sağdaki)' olsa, parametre gereksiz yere kopyalanırdı. (Tam tersine, aşağıdaki güzelliğin temeli de buna dayalı! :))

Buraya kadar da tamam... :)

Bundan sonrası, ifadelerin örneğin geçici değerlerinde karşımıza çıkan rvalue değerlerle ilgili. Benim bu yazıyı yazmamın nedeni de o... :)

Şimdi yukarıdaki opAssign'ı geçici bir nesne ile kullanalım:

import std.stdio;

struct Yapı
{
   int[] dizi;

   this(int uzunluk)
   {
       dizi.length = uzunluk;
   }

   ref Yapı opAssign(const ref Yapı sağdaki)
   {
       // sağdakinin kopyasına eşitliyoruz
       dizi = sağdaki.dizi.dup;
       return this;
   }
}

void main()
{
   auto y1 = Yapı(10);

   y1 = Yapı(20);   /* sağdaki bir rvalue'dur (çok kabaca, ve tam doğru
                     * olmadan 'geçici nesne' olarak açıklayabiliriz)
                     */
}

O da çok güzel ama bir sorun var: sağdaki geçici Yapı(20) nesnesi zaten hemen o ifadenin sonunda yok olmak üzere olduğu halde, opAssign'ın içinde .dup ile bir kopyalama yapıyoruz. İsraf... :)

Burada kendim bir sorun daha keşfediyorum: eğer o geçici nesne bir işlevin dönüş değeri ise, hem yine israf, hem de derlenemiyor bile:

import std.stdio;

struct Yapı
{
   int[] dizi;

   this(int uzunluk)
   {
       dizi.length = uzunluk;
   }

   ref Yapı opAssign(const ref Yapı sağdaki)
   {
       // sağdakinin kopyasına eşitliyoruz
       dizi = sağdaki.dizi.dup;
       return this;
   }
}

void main()
{
   auto y1 = Yapı(10);

   y1 = Yapı(20);   /* sağdaki bir rvalue'dur (çok kabaca, ve tam doğru
                     * olmadan 'geçici nesne' olarak açıklayabiliriz)
                     */

   y1 = foo();  /* burada da sağdaki yine bir rvalue
                 *
                 * (zaten rvalue "right value"dan gelir ve "eşitliğin
                 * yalnızca sağ tarafında bulunabilen değer" anlamındadır)
                 */
}

Yapı foo()
{
   return Yapı(30);
}

O kod derlenemiyor. Doğrusu bence bu bir derleyici hatası; çünkü foo'nun döndürdüğü nesnenin de geçici bir nesne olarak opAssign'ın 'const ref' parametresine bağlanabilmesi gerekir.

O kadar önemli değil; çünkü bu israfı ortadan kaldıran aşağıdaki yöntem, foo'nunki gibi dönüş değerlerinde de işe yarıyor.

Daha ileri gitmeden önce, bu konunun C++'nın en bilinen sorunlarından olduğunu da söylemeliyim. O yüzden C++0x'e "move constructor" denen bir kavram eklediler. D'nin zaten desteklediği ve "geçici nesnelerden kopyalamak yerine, onların malını kendimize geçirelim" diye açıklanabilecek aşağıdaki yöntemin aynısıdır.

C++'da "move constructor" kavramı üzerinde en çok çalışan ve C++'nın olanakları dahilinde yarım çözümler bulabilen de Andrei Alexandrescu'dur. O yüzden bu konunun D'de doğru çalışması şaşırtıcı olmamalı. ;)

Amaç şu: Sağ tarafta normal bir nesne olduğunda, onun dizisini yukarıdaki gibi kopyalamak istiyoruz. Ama, sağ tarafta geçici bir nesne olduğunda, veya daha doğru dille, bir rvalue olduğunda, kopyalamak değil, onun malını kendimize geçirmek istiyoruz. Çünkü o geçici nesne zaten opAssign'dan çıkıldığı an yok olacaktır; o yüzden bu üstümüze mal geçirme işleminde sakınacak bir şey olamaz.

Yöntem şu: yukarıda yazdığımız opAssign'ın parametresini kopya alacak şekilde değiştiriyoruz: '(Yapı sağdaki)' olarak tanımlıyoruz. Böylece hem normal (lvalue) nesneler, hem de rvalue nesnelerin derdi halloluyor:

   /*
    * Sağda geçici nesne olduğu durumda, bu parametre kopyalama işi de
    * derleyici tarafından atlanır.
    */
   ref Yapı opAssign(Yapı sağdaki)
   {
       /*
        * swap, değiş tokuş etme işlecidir. Burada bizim üyemizi
        * sağdakinin üyesiyle değiş tokuş ediyoruz. Böylece kopya için
        * zaman harcanmamış oluyor.
        *
        * (swap std.algorithm'de tanımlıdır ve her türle çalışır)
        */
       swap(dizi, sağdaki.dizi);
       return this;
   }

Çalışma nedeni şu: Sağ tarafta normal nesne kullanıldığında zaten opAssign'ın içinde kopyalıyorduk ya... İşte zaten kopyalama işlemi parametrenin kopyalanması sırasında hallediliyor. Çok güzel... :) Bizim ayrıca üyeleri .dup diye veya başka yöntemlerle teker teker kendimiz kopyalamamız gerekmiyor.

Sağ tarafta rvalue kullanıldığında ise, çok önemli olarak, derleyici parametre kopyalama adımını atlıyor ve geçici nesneyi zaten o parametrenin yerinde kuruyor. Bu, C++'da da dilin tanımladığı iki eniyileştirme yöntemidir: "return value optimization (RVO)" ve "named return value optimization (NRVO)" adıyla geçer.

Neler olup bittiğini de anlatan bütün program şöyle:

import std.stdio;
import std.algorithm;
import std.string;

struct Yapı
{
   int[] dizi;

   this(int uzunluk)
   {
       dizi.length = uzunluk;
       writeln(kimlik, " kuruldu");
   }

   /*
    * Bunu yalnızca kopyanın olduğu anı belgeyebilmek için yazdık
    */
   this(this)
   {
       writeln(kimlik, " kopyalandı");
   }

   @property string kimlik() const
   {
       return format("%s elemanlı nesne", dizi.length);
   }

   /*
    * Sağda geçici nesne olduğu durumda, bu parametre kopyalama işi de
    * derleyici tarafından atlanır.
    */
   ref Yapı opAssign(Yapı sağdaki)
   {
       /*
        * swap, değiş tokuş etme işlecidir. Burada bizim üyemizi
        * sağdakinin üyesiyle değiş tokuş ediyoruz. Böylece kopya için
        * zaman harcanmamış oluyor.
        *
        * (swap std.algorithm'de tanımlıdır ve her türle çalışır)
        */
       writeln(kimlik, ", ", sağdaki.kimlik, " ile değiş tokuş ediliyor");
       swap(dizi, sağdaki.dizi);
       return this;
   }
}

void main()
{
   auto y1 = Yapı(10);
   auto y2 = Yapı(20);

   /* Bu, kopyalama gerektirir (çıktıda yalnızca bunun kopyalandığını
    * görüyoruz) */
   y1 = y2;
   assert(y2.dizi.length == 20);

   /*
    * Aşağıdakilerin ikisinin parametre olarak kopyalanmaları derleyici
    * tarafından atlanır. Çünkü zaten geçicidirler. Kendileri, kopyaları
    * olarak kullanılırlar. O yüzden çıktıda bunlarla ilgili kopya
    * görmüyoruz.
    */

   y1 = Yapı(30);   /* sağdaki bir rvalue'dur (çok kabaca, ve tam doğru
                     * olmadan 'geçici nesne' olarak açıklayabiliriz)
                     */

   y1 = foo();  /* burada da sağdaki yine bir rvalue
                 *
                 * (zaten rvalue "right value"dan gelir ve "eşitliğin
                 * yalnızca sağ tarafında bulunabilen değer" anlamındadır)
                 */
}

Yapı foo()
{
   return Yapı(40);
}

Çıktısı şöyle:

10 elemanlı nesne kuruldu
20 elemanlı nesne kuruldu
20 elemanlı nesne kopyalandı
10 elemanlı nesne, 20 elemanlı nesne ile değiş tokuş ediliyor
30 elemanlı nesne kuruldu
20 elemanlı nesne, 30 elemanlı nesne ile değiş tokuş ediliyor
40 elemanlı nesne kuruldu
30 elemanlı nesne, 40 elemanlı nesne ile değiş tokuş ediliyor

Yani aslında D.ershane'nin opAssign'ının da bu şekilde değiştirilmesi gerekecek. :) Parametrenin 'ref' veya 'const ref' değil, kopyalan şekilde yazılması gerekiyor.

Ali

--
[ Bu gönderi, http://ddili.org/forum'dan dönüştürülmüştür. ]