İşlev Nesneleri ('Function Objects' veya 'Functors')

Bu yazıda işlev nesnelerini tanıtacak ve onların standart algoritmalarla nasıl kullanıldıklarını göstereceğim. Standart algoritma olarak gerekliliği tartışma konusu olan 'for_each'i kullanacak, ve bu sırada az da olsa felsefe bile yapacağım :) Son olarak, işlev nesnelerinin özel bir hali olan karar nesnelerini (predicates) tanıtacağım.

C++'ın işleç yükleme (operator overloading) olanağı sayesinde işlev çağırma işleci olan 'operator()' işlecini de yükleyebiliriz. Bu işleç, bir sınıfın nesnelerinin sanki işlevmişler gibi çağrılabilmelerini sağlar. Örneğin, 'Sinif' adında bir sınıf için 'operator()' işlecinin tanımlanmış olduğunu varsayarsak, o sınıftan bir nesneyi şöyle çağırabiliriz:

    Sinif nesne;
    nesne();  // <-- Nesne, islev gibi kullaniliyor

Bu işlecin tanımlanması, yazımı diğer işleçlerinkinden biraz daha karışık olduğu için baştan benim kafamı karıştırmıştı. Bunun nedeni, işlecin bildiriminde iki çift parantez bulunmasıdır. Birinci çift, işlecin adının parçası olan parantezler; ikinci çift ise tanımlamakta olduğumuz işlecin aldığı parametrelerin listesini belirleyen parantezlerdir.

Bu karışıklığı, nesneleri işlev gibi çağrıldıklarında ekrana "Merhaba dunya!" yazan bir sınıfın tanımında görebiliriz:

#include <iostream>

class Merhaba
{
public:

    // Iste iki cift parantez:

    void operator() () const
    {
        std::cout << "Merhaba dunya!\n";
    }
};

int main()
{
    Merhaba merhaba;
    merhaba();
}

'operator()' işlecinin tanımındaki birimleri kısaca şöyle anlatabilirim:

  void:       bu işlev hiçbir değer döndürmüyor
  operator(): tanımladığımız işlecin adı
  ():         parametre listesi; bu durumda boş
  const:      bu işlev nesnenin kendisini değiştirmiyor

Şimdi, aynı sınıfa bir de parametre alan bir 'operator()' ekleyeceğim:

#include <iostream>
#include <string>

using namespace std;

class Merhaba
{
public:

    void operator() () const
    {
        std::cout << "Merhaba dunya!\n";
    }

    void operator() (string const & kim) const
    {
        std::cout << "Merhaba " << kim << "!\n";
    }
};

int main()
{
    Merhaba merhaba;
    merhaba();
    merhaba("Can");
    merhaba("Ebru");
}

İkinci işlevin parametre olarak sabit bir 'string' referansı aldığını parametre listesinin içinde bildirmiş olduk.

Şimdi biraz daha ileri giderek 'Merhaba' sınıfını hiç olmazsa biraz olsun akıllı bir hale getireceğim. 'Merhaba' sınıfının nesneleri hep standart çıkışa yazdırmak yerine, artık kurulurlarken kendilerine bildirilen bir çıkış akımına yazdıracaklar. Ek olarak, "Merhaba" yerine başka bir söz, örneğin "Selam" da kullanabileceğiz:

#include <iostream>
#include <string>
#include <fstream>

using namespace std;

class Merhaba
{
    ostream & cikis_;
    string soz_;
    
public:

    /*
       Hangi cikis akimina yazdiracagini ve hangi
       sozu kullanacagini artik soyleyebiliyoruz
    */

    Merhaba(ostream & cikis, string const & soz)
        :
        cikis_(cikis),
        soz_(soz)
    {}

    void operator() () const
    {
        cikis_ << soz_ << " dunya!\n";
    }

    void operator() (string const & kim) const
    {
        cikis_ << soz_ << ' ' << kim << "!\n";
    }
};

void standartCikisla()
{
    // Burasi onceki ornekler gibi

    Merhaba merhaba(cout, "Merhaba");
    merhaba();
    merhaba("Can");
}

void kutukle()
{
    // Ama baska akimlar da kullanabiliriz

    ofstream kutuk("islev_nesnesi_deneme");

    Merhaba merhaba(kutuk, "Selam");
    merhaba();
    merhaba("Ebru");
}

int main()
{
    standartCikisla();
    kutukle();
}

Bu programı bir sorun çıkmadan çalıştırıp denediğinizde 'islev_nesnesi_deneme' adlı bir kütük oluşmuş olacak. O kütüğü içine baktıktan sonra silmek isteyebilirsiniz.

Buraya kadar gösterdiklerimin işlev nesnelerinin yararları konusunda iyi örnekler olmadıklarını biliyorum. Onların sıradan işlevlerle karşılaştırıldıklarında hiçbir yarar sağlamadıklarını düşünülebilirsiniz. Herhangi bir çıkış akımına bir selamlama ifadesi yazdırma işini sıradan bir işlevle de halledebilirdik:

void merhaba(ostream & cikis,
             string const & soz,
             string const & kim)
{
    cikis << soz << ' ' << kim << "!\n";
}

Bu çözüm gerçekten de çok daha temiz ve anlaşılır oldu. Ancak, sıradan işlev kullanan çözümde; kullanılacak olan çıkış akımını ve selamlama sözünü, işlevi çağırdığımız her yerde bilmemiz ve açıkça yazmamız gerekir. Ayrıca, o işi yapabilmek için nelerin gerektiğini de bilmek zorundayızdır. Örneğin, programdan beklentilerin arttığını ve yazının artık belirli bir renkte yazdırılması gerektiğini düşünün. Bu durumda, işlevi çağıran her noktaya o renk bilgisinin taşınması ve işlevin onunla çağrılması gerekir.

İşlev nesnelerinin üstünlüğü; yapacakları işlemler için gereken bilgileri kendileri tutup kullandıkları için, o bilgileri programın geri kalanından soyutlamalarıdır. Renk örneğini işlev nesnelerine uyguladığımızda, değişikliğin yalnızca nesne(ler)in kuruldukları nokta(lar)da yapılmaları gerektiğini görebiliriz. O nesneyi işlev gibi kullanan hiçbir noktada değişiklik yapmaya gerek kalmaz.

İşlev nesnelerini C++'ın şablon olanağı ile birlikte kullandığımızda çok etkili soyutlamalar kurabiliriz. [1]

Standart kütüphanedeki bazı algoritmalar bu tür soyutlamalardan yararlanılarak yazılabilmişlerdir. Örneğin; standart algoritmaların en basiti olarak tanınan ve bir dizi nesnenin her birisi üzerinde belirli bir işlem yapmak için kullanılan std::for_each algoritması, uygulayacağı işlemin tam olarak ne türden olduğunu bilmek zorunda değildir. Bu yüzden, bir işlev gibi kullanılabilen herşey, örneğin bir işlev nesnesi de std::for_each ile çalışabilir. [2]

Her standart C++ derleyicisi ile gelen ve <algorithm> başlığı içinde tanımlanan for_each algoritmasının son derece basit olan tanımı şöyle verilebilir:

template <class Erisici, class Islem>
Islem for_each(Erisici bas, Erisici son, Islem islem)
{
    for ( ; bas != son; ++bas)
    {
        islem(*bas);
    }

    return islem;
}

Şablon kodlarında kullanılan türlerin hangi koşulları sağlamaları gerektiğini koda bakarak anlayabiliriz:

for_each algoritmasıyla kullanılabilecek Erisici türünün sağlaması gereken koşullar şunlardır:

Erişiciler (iterators) bu yazının asıl konusu olmadıkları için, yalın göstergelerin bile Erisici olarak for_each'e gönderilebileceklerini söylemekle yetineceğim. [3]

for_each'in tanımında kullanılan Islem türünün sağlaması gereken koşullar da şöyledir:

Islem türünün koşullarına baktığımızda; Erisici::operator* işlecinin dönüş türünden bir parametre alan sıradan bir işlevin bile kullanılabileceğini görebiliriz. [4]

Bu koşulları somut bir örnekte sağlamak için, for_each'in üzerlerinde çalışacağı nesnelerin türünü 'int' olarak seçecek ve onları bir C dizisine yerleştireceğim:

    int const sayilar[] = { 60, 60, 24, 7, 12 };

Bu durumda, sayıların başını belirlemek için 'sayilar' dizisinin adını, sayıların sonunu belirlemek için de 'sayilar + TOPLAM_OGE(sayilar)' adresini kullanabiliriz.

for_each'i kullanabilmek için gereken son şey, '*sayilar' işleminin dönüş türünü parametre olarak alan bir işlev bulmaktır. 'sayilar', for_each işlevine gönderildiğinde 'int *' türünde olduğu için, '*sayilar' ifadesinin türü 'int' olur.

Yani, bu durumda for_each algoritması ile çalışabilecek bir işlevin 'int' türünde bir parametre alması yeterlidir. [4]

Bütün bunları bir araya getirdiğimizde şöyle bir program yazabiliriz:

#include <iostream>
#include <algorithm>

using namespace std;

void cikisaYazdir(int a)
{
    cout << a << ' ';
}

#define TOPLAM_OGE(x) (sizeof(x) / sizeof(*(x)))

int main()
{
    int sayilar[] = { 60, 60, 24, 30, 12 };
    
    for_each(sayilar,
             sayilar + TOPLAM_OGE(sayilar),
             cikisaYazdir);

    cout << '\n';
}

Bu örnekte for_each'in 'int' alan ve bir işlev gibi çağrılabilen her türle çalıştığını bildiğimize göre, tekrar bu yazının konusuna dönecek ve onu şimdi de bir işlev nesnesi ile birlikte kullanacağım. Örneği işe yarar hale getirmek için, for_each ile birlikte kullanacağım nesneyi yine az da olsa akıllı yapacağım: hangi çıkış akımına yazdıracağını ve sayıları ne tür ayraçlar arasına alacağını da bilecek:

#include <iostream>
#include <algorithm>

using namespace std;

class AyraclaYazdiran
{
    ostream & cikis_;

    // Ayraclar
    char acma_;
    char kapama_;

public:

    AyraclaYazdiran(ostream & cikis,
                    char acma, 
                    char kapama)
        :
        cikis_(cikis),
        acma_(acma),
        kapama_(kapama)
    {}

    void operator() (int sayi) const
    {
        cikis_ << acma_ << sayi << kapama_;
    }
};
        
#define TOPLAM_OGE(x) (sizeof(x) / sizeof(*(x)))

int main()
{
    int sayilar[] = { 60, 60, 24, 30, 12 };

    /*
      Iki degisik islev nesnesi olusturuyoruz.
      Bu nesnelerden birisi kendisine operator() ile
      gonderilen sayiyi normal parantezler icine
      alacak; digeri de kume parantezleri icine...
     */
    AyraclaYazdiran parantezleYazdir(cout, '(', ')');
    AyraclaYazdiran kumeyleYazdir(cout, '{', '}');
    
    for_each(sayilar,
             sayilar + TOPLAM_OGE(sayilar),
             parantezleYazdir);

    cout << '\n';

    for_each(sayilar,
             sayilar + TOPLAM_OGE(sayilar),
             kumeyleYazdir);

    cout << '\n';
}

Bu program standart çıkışa şunları gönderir:

  (60)(60)(24)(30)(12)
  {60}{60}{24}{30}{12}

Yukarıdaki örnekteki 'parantezleYazdir' ve 'kumeyleYazdir' nesneleri, for_each'e gönderilmek dışında bir amaçla kullanılmıyorlar. Böyle durumlarda açıkça nesne oluşturmak yerine, for_each'i geçici nesnelerle çağırırız. Örneğin:

    for_each(sayilar,
             sayilar + TOPLAM_OGE(sayilar),
             AyraclaYazdiran(cout, '(', ')'));

Yukarıdaki satırda, daha for_each çağrılmadan önce geçici bir AyraclaYazdiran nesnesinin oluşturulduğuna dikkat edin. for_each, o geçici işlev nesnesinin bir kopyasını alır ve kendi içindeki döngü içinde her bir sayıyı o nesnenin operator() işlecine gönderir.

Buraya kadar for_each'in dönüş türünü hiç kullanmadık. Gerektiği zaman, for_each'in döndürdüğü işlev nesnesini kullanmak veya incelemek isteyebiliriz. O zaman yapmamız gereken, for_each'in döndürdüğü nesneden yararlanmaktır:

    AyraclaYazdiran yazdiran = for_each(/* ... */);

Şimdi 'yazdiran' adlı nesneyi kullanabiliriz. Örneğin kaç kere yazdırdığını bildiren bir işlevi olduğunu varsayarsak:

    cout << yazdiran.kacKere() << '\n';

for_each gerçekten gerekli midir

for_each kadar basit bir algoritmanın gerekliliği tartışma konusudur. Onun gereksiz olduğunu savunanlara göre, sırf for_each'i kullanabilmek için sınıf tanımlamak bir külfettir. Açıkça yazılmış bir for döngüsünün daha kolay okunur olduğunu düşünülebilir:

#include <iostream>

using namespace std;

#define TOPLAM_OGE(x) (sizeof(x) / sizeof(*(x)))

int main()
{
    int sayilar[] = { 60, 60, 24, 30, 12 };

    for (int i = 0; i != TOPLAM_OGE(sayilar); ++i)
    {
        cout << '(' << sayilar[i] << ')';
    }

    cout << '\n';

    for (int i = 0; i != TOPLAM_OGE(sayilar); ++i)
    {
        cout << '{' << sayilar[i] << '}';
    }

    cout << '\n';
}

Gerçekten de bu program bir öncekinden çok daha kısa oldu. operator() işleci gibi bir kavramla da uğraşmak zorunda kalmadık. Hatta kod, döngüleri bir işleve taşıyarak daha da okunur bir hale getirilebilirdi.

Ancak, soyutlamayı ve işlemleri uygun şekillerde adlandırmayı programcılığın önemli parçaları olarak düşünürsek, son yazdığım örneğin bu konuda başarılı olamadığını görürüz. Çünkü, program "her birisini parantezlerle yazdırma" ve "her birisini küme parantezleriyle yazdırma" kavramlarını açıkça ifade edemiyor. O kavramlar, kodun içinden ancak kod incelenerek çıkartılabiliyorlar. Kodu okuyan her programcı, kavramları kendi kafasında tekrar tekrar oluşturmak zorunda kalıyor:

"Tamam, 'sayilar'da baştan sona ilerleyeceğiz; her bir sayıyı etrafında parantezler olacak şekilde yazdıracağız... Buradaki döngü de bir öncekinin aynısı ama bu küme parantezleri kullanıyor."

Eğer döngüleri az önce söylediğim gibi tek bir işleve taşırsak, kodun ifade yeteneğini arttırırız; ama hiç olmazsa "her birisi için" kavramının kafada tekrar kurulmasını kodu okuyana bırakırız.

Öte yandan, programcılar 'for' döngüsünün yapısına o kadar alışıktırlar ki, bir bakışta zaten bütün nesneler üzerinde bir işlem yapıldığını görebilirler. Dediğim gibi, for_each tartışmalı bir algoritmadır; ve bu kadar felsefe yeter. :)

İşlev nesnelerini başka algoritmalarla kullanmadan önce, for_each'in for döngülerinden daha iyi olduğunu düşündüren bir örnek vermek istiyorum. Son kullandığım programı, bir C dizisi yerine standart topluluklardan birisini, örneğin std::list'i kullanacak şekilde değiştiriyorum:

#include <iostream>
#include <list>

using namespace std;

#define TOPLAM_OGE(x) (sizeof(x) / sizeof(*(x)))

// Okunurlugu arttirmak icin bir ad takiyorum:
typedef list<int> Sayilar;

Sayilar sayilariKur()
{
    int sayilar[] = { 60, 60, 24, 30, 12 };
    Sayilar sonuc(sayilar, sayilar + TOPLAM_OGE(sayilar));
    return sonuc;
}

int main()
{
    Sayilar sayilar = sayilariKur();

    for (Sayilar::const_iterator it = sayilar.begin();
         it != sayilar.end(); ++it)
    {
        cout << '(' << *it << ')';
    }

    cout << '\n';

    for (Sayilar::const_iterator it = sayilar.begin();
         it != sayilar.end(); ++it)
    {
        cout << '{' << *it << '}';
    }

    cout << '\n';
}

Buradaki 'for' döngülerinin anlaşılması bana zor gelmeye başladı. Çoğu zaman, okunurluğu arttırmak için topluluk erişicilerine de adlar takarız:

  typedef Sayilar::iterator SayiErisici;
  typedef Sayilar::const_iterator SayiSabitErisici;

Ondan sonra da for ifadesini şöyle değiştiririz:

    for (SayiSabitErisici it = /* ... */)

Karşılaştırma olsun diye, işlev nesnesi kullanan programın 'main' işlevinin std::list kullanıldığı durumda nasıl olacağını göstermek istiyorum:

int main()
{
    Sayilar sayilar = sayilariKur();
    
    for_each(sayilar.begin(),
             sayilar.end(),
             AyraclaYazdiran(cout, '(', ')'));

    cout << '\n';

    for_each(sayilar.begin(),
             sayilar.end(),
             AyraclaYazdiran(cout, '{', '}'));

    cout << '\n';
}

Buradaki önemli bir nokta, elimizi topluluktaki nesnelerin türlerine bulaştırmak zorunda kalmıyor oluşumuzdur. for_each gibi türden bağımsız algoritmalar kullandığımızda, 'SayilarSabitErisici' gibi yeni tür adları tanımlamak ve onları kullanmak zorunda kalmayız.

Karar Nesneleri (Predicates)

Bundan önceki örneklerdeki işlev nesnelerinde operator() işleci hiçbir değer döndürmüyordu. O yüzden dönüş türünü 'void' olarak tanımlamıştım.

Bazı durumlarda, operator() işleci belirli bir işlemin yapılıp yapılmayacağı kararını vermek için kullanılır. İşlecin dönüş türünün 'bool' olarak tanımlandığı bu tür sınıfların nesnelerine 'karar nesneleri' (predicates) denir. Geleneksel olarak, 'true' dönüş değeri kararın olumlu olduğunu, 'false' ise olumsuz olduğunu bildirir.

Karar düzeneğini algoritmanın içinden çıkartarak bir işlev veya işlev nesnesine taşımak yoluyla soyutlamak, algoritmanın kullanım alanını genişletir.

Standart 'find_if' algoritması bunun güzel bir örneğidir. Bu algoritma, belirli bir koşulu sağlayan ilk nesneyi kendisine verilen bir dizi nesne arasında arar. Nesnenin o belirli koşulu sağlayıp sağlamadığının kararı, algoritmaya gönderilen bir karar nesnesi ile verilir:

#include <algorithm>
#include <vector>
#include <iostream>
#include <string>
#include <iterator>

using namespace std;

/*
  Okunurlugu arttirmak icin 'typedef'ler
 */
typedef vector<string> Sozcukler;
typedef Sozcukler::const_iterator SozcukErisici;    

/*
  Bu karar sinifinin nesneleri, kendilerine
  verilen bir 'string'in belirli bir harfinin
  belirli bir degere esit olup olmadigini
  belirtmek icin kullanilirlar.

  Ornek kullanim:

     HarfiEsittir basHarfi_a(0, 'a');
     assert(basHarfi_a("ali"));

*/
class HarfiEsittir
{
    /*
      Nesne, kuruldugu zaman kendisine bildirilen
      harfin sozcuk icindeki sirasini ve o harfi
      hangi degerle karsilastiracagini aklinda
      tutacak.
     */
    size_t hangisi_;
    char harf_;

public:

    HarfiEsittir(size_t hangisi, char harf)
        :
        hangisi_(hangisi),
        harf_(harf)
    {}

    /*
      Karar nesnesi, kendisine gonderilen her
      bir 'string'in bu sinifin isi olan kosulu
      saglayip saglamadiginin kararini verecek.
     */
    bool operator() (string const & sozcuk) const
    {
        return ((sozcuk.length() > hangisi_)
                &&
                (sozcuk[hangisi_] == harf_));
    }
};

void uyanSozcuguYazdir(Sozcukler const & sozcukler,
                        size_t hangisi,
                        char harf)
{
    cout << (hangisi + 1) << ". harfi "
         << harf << " olan sozcuk";

    /*
      find_if algoritmasi, belirtilen araliktaki
      nesneler arasinda belirli bir kosulu
      saglayan ilk nesneyi dondurur.
      
      Nesneleri sirayla karar nesnesine gonderir
      ve karar nesnesinin 'true' dondurdugu ilk
      nesnede aramayi durdurur.

      Karar nesnesinin butun nesneler icin 'false'
      dondurdugu gibi bir durumda; find_if'in donus
      degeri araligin sonunu belirleyen erisicidir.

      (Bu durumda araligin sonu 'sozcukler.end()'dir.)
     */

    SozcukErisici erisici
        = find_if(sozcukler.begin(),
                  sozcukler.end(),
                  HarfiEsittir(hangisi, harf));

    /*
      Yukaridaki kullanimda karar nesnesini
      adsiz bir gecici nesne olarak kullandigima
      dikkat edin. Onun yerine, acikca bir nesne
      de olusturabilirdim:

        HarfiEsittir karar(hangisi, harf);
        SozcukErisici erisici
            = find_if(sozcukler.begin(),
                      sozcukler.end(),
                      karar);

      Ancak, yalnizca bir kere kullanilip atilacak
      olan o karar nesnesini adlandirmanin burada
      bir yarar saglayacagini dusunmedim.
     */

    if (erisici == sozcukler.end())
    {
        cout <<  " bulunamadi";
    }
    else
    {
        cout << ": " << *erisici;
    }

    cout << '\n';
}

int main()
{
    Sozcukler sozcukler;

    /*
      Standart giristeki butun sozcukleri okuyoruz.

      Standart girisin klavyeye bagli oldugu
      durumda girisi sonlandirmak icin
      Linux'ta Ctrl-D, Windows'da ise Ctrl-Z
      tuslarina basmaniz gerekebilir.
    */
    copy(istream_iterator<string>(cin),
         istream_iterator<string>(),
         back_inserter(sozcukler));

    uyanSozcuguYazdir(sozcukler, 0, 'a');
    uyanSozcuguYazdir(sozcukler, 2, 'z');
}

Daha önce de değindiğim gibi, karar nesneleriyle çalışan bütün algoritmalar sıradan işlevlerle de çalışabilirler. Çünkü, bu tür algoritmaların karar soyutlamasından tek beklentileri şunlardır:

Son örneği işlev nesneleri yerine sıradan işlevlerle kullanabilmek için şöyle iki işlev tanımlamamız gerekir:

  bool birinciHarfi_a(string const & sozcuk)
  {
      size_t const hangisi = 0;
      char const harf = 'a';
      
      return ((sozcuk.length() > hangisi)
              &&
              (sozcuk[hangisi] == harf));    
  }
  
  bool ucuncuHarfi_z(string const & sozcuk)
  {
      size_t const hangisi = 2;
      char const harf = 'z';
      
      return ((sozcuk.length() > hangisi)
              &&
              (sozcuk[hangisi] == harf));    
  }

Daha sonra, find_if algoritmasını örneğin şöyle kullanabiliriz:

    SozcukErisici erisici
        = find_if(sozcukler.begin(),
                  sozcukler.end(),
                  birinciHarfi_a);

Görüldüğü gibi, karar vermek için sıradan işlevler kullanmak, her bir koşul için ayrı bir işlev tanımlamayı gerektirir. Buna bakarak, işlev nesnelerinin getirdikleri esneklik açısından sıradan işlevlerden çok daha üstün olduklarını söyleyebiliriz; çünkü bir kere yazdığımız HarfiEsittir sınıfını sonsuz sayıda değişik karar vermek için kullanabiliriz.

Sıradan işlev kullanma konusunda ısrarlı olan programcıların böyle bir durumda makro sanatına (!) başvurarak her bir koşul için ayrı işlev yazmaktan kurtulabileceklerini biliyorum. Onlara bu zorlu sanatta başarılar dilerim :) [5]

Özet

'operator()' işlecinin tanımlanmış olduğu sınıfların nesneleri, işlev gibi çağrılabildikleri için 'işlev nesneleri' ('function objects' veya 'functors') olarak adlandırılırlar. İşlev nesnelerinin, 'operator()' işleci 'bool' döndürdüğü için karar alma amacıyla kullanılabilen özel türlerine 'karar nesneleri' (predicates) denir.

İşlev nesneleri; kodun esnekliğini ve okunurluğunu, ve koddaki soyutlama olanaklarını büyük ölçüde arttırırlar. Özellikle bu konuda inandırıcı olduğumu umuyorum :)

Bundan sonraki yazımda anlatmak istediğim ve standart topluluk şablonları arasında benim belki de en sık kullandığım 'std::map' de içindeki nesneleri sıralama kararını işlev nesnelerine bırakır. Burada anlattıklarımın hiç olmazsa o yazıda işe yarayacağını düşünüyorum.

Referanslar

[1] Şablonlar hakkında küçük bir tanıtım için 'Türden bağımsız bir toplama algoritması' yazısını okumak isteyebilirsiniz:

http://ceviz.net/index.php?case=article&id=170

[2] 'for_each'in adı, "her birisi için" anlamına gelen İngilizce "for each" ifadesinden gelir ve "for iiç" gibi okunur.

[3] Erişiciler hakkında biraz daha fazla bilgi edinmek için şu yazıdan yararlanabilirsiniz:

http://ceviz.net/index.php?case=article&id=184

[4] Aslında o parametrenin türüne otomatik olarak dönüşebilen türlerden alan işlevler de kullanılabilirler. Parametrenin örneğin 'int' olması şart değildir. Onun yerine, örneğin; 'int &', 'int const &', veya otomatik olarak 'int'ten dönüştürebilen temel bir tür veya bir kullanıcı türü de bu örnekte for_each'e gönderilecek işlevin parametresi olabilirler.

[5] Makroların zararları konusunda İngilizce bilgi edinmek isteyenler, google.com'da "macros are evil" aramasının sonuçlarından yararlanabilirler.


Ali Çehreli - acehreli@yahoo.com