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

Cansiz

New member
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:
PHP:
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:
PHP:
#include <iostream>

class Merhaba
{
public:

    // Iste iki cift parantez:

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

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:

PHP:
#include <iostream>
#include <string>

using namespace std;

class Merhaba
{
public:

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

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

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:
PHP:
#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!
";
    }

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

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:
PHP:
void merhaba(ostream & cikis,
             string const & soz,
             string const & kim)
{
    cikis << soz << ' ' << kim << "!
";
}
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:

PHP:
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:

- Kopyalanabilmelidir; çünkü 'bas' ve 'son', parametre listesinde referans veya gösterge olarak değil, değer olarak gönderiliyorlar

- İki Erisici nesnesi operator!= işleci ile karşılaştırılabilmelidir; çünkü 'bas != son' ifadesi kullanılmış

- Nesnelerine arttırma işleci uygulanabilmelidir; çünkü '++bas' ifadesi kullanılmış

- Nesnelerine operator* işleci uygulanabilmelidir; çünkü '*bas'
ifadesi kullanılmış

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:

- Kopyalanabilmelidir; çünkü 'islem' hem parametre listesinde değer olarak geçiyor, hem de for_each'ten kopyalanarak döndürülüyor

- Erisici'nin operator* işlecinin dönüş türü ne ise, Islem türü için ona uygun bir operator() işleci tanımlanmış olmalıdır; çünkü 'islem(*bas)' ifadesi kullanılmış

Islem türünün koşullarına baktığımızda; Erisici::eek:perator* 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:
PHP:
#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 << '
';
}
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:
PHP:
#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 << ' ';    for_each(sayilar,             sayilar + TOPLAM_OGE(sayilar),             kumeyleYazdir);    cout << ' ';}
Bu program standart çıkışa şunları gönderir:
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:
PHP:
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:
PHP:
  cout << yazdiran.kacKere() << ' ';
alıntıdır!!
 

Cansiz

New member
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:
PHP:
#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 << '
';

    for (int i = 0; i != TOPLAM_OGE(sayilar); ++i)
    {
        cout << '{' << sayilar[i] << '}';
    }
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:
PHP:
#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 << ' ';    for (Sayilar::const_iterator it = sayilar.begin();         it != sayilar.end(); ++it)    {        cout << '{' << *it << '}';    }    cout << ' ';}
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:

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

    cout << '
';

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

    cout << '
';
}
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:
PHP:
#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 << ' ';}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:

- 'bool' döndürmeleri

- algoritmanın üzerinde çalıştığı nesneye uygun bir parametre almaları

Son örneği işlev nesneleri yerine sıradan işlevlerle kullanabilmek için şöyle iki işlev tanımlamamız gerekir:
PHP:
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:

PHP:
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]

Alıntıdır!!
 

HTML

Üst