Cansiz
New member
- Katılım
- 7 Ocak 2006
- Mesajlar
- 2,048
- Reaction score
- 0
- Puanları
- 0
- Yaş
- 35
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:
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:
'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:
İ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:
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:
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:
Ş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:
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:
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:
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:
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:
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
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();
}
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");
}
Ş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();
}
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 << "!
";
}
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;
}
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:
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 << '
';
}
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:
(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, '(', ')'));
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!!