Anonim Fonksiyonlar
Anonim fonksiyonlar C++11 standartları ile dile eklenmiş isimsiz içsel fonksiyonlardır.
Anonim fonksiyonlar, ayrıca lambda fonksiyonları olarak da adlandırılır ve lambda ifadeleri (lambda expression) kullanılarak tanımlanırlar. Bir lambda ifadesi aşağıdaki forma sahiptir.
[capture-list] (parameters) -> return_type {function_body}
Örnek bir anonim fonksiyon tanımı ise aşağıdaki gibidir.
[] (int x, int y) -> int {return x + y}
Anonim fonksiyonlar, kendilerini sarmalayan fonksiyonun yerel değişkenlerine ulaşabilmektedir. Bu özelliğin GNU C eklentisi olarak nasıl gerçeklendiğini bir önceki bölümde incelemiştik. Burada ise benzer özellik fonksiyon nesneleri (function object) kullanılarak sağlanmaktadır.
Derleyici lambda ifadesini kullanarak yeni bir tür tanımlar ve bu tür için bir operator() fonksiyonu yazar. Anonim fonksiyona ilişkin işlemler bu nesne kullanılarak yapılır. Bu isimsiz fonksiyon nesneleri ayrıca closure olarak da isimlendirilmektedir.
Not: Bir operator() fonksiyonu tanımlayarak, fonksiyon çağrı operatorünü (function call operator) yükleyen (overload) sınıf örneklerine yani bu türden oluşturulan nesnelere fonksiyon nesneleri (function object) veya functor denilmektedir.
Fonksiyon nesneleri, söz dizimsel olarak, görünüşte birer fonksiyon gibi kullanılabilir ve başka fonksiyonlara callback olarak geçirilebilir.
Fonksiyon nesneleri sahip oldukları veri elemanları sayesinde durum (state) bilgisine sahip fonksiyonlar olarak kullanılabilirler. Durum bilgisinin kullanımını göstermek için aşağıdaki örneği inceleyelim.
#include <iostream>
using namespace std;
class Functor {
public:
Functor(int state) {
m_member = state;
}
bool operator() (int a, int b) {
return (a - b) > m_member;
}
private:
int m_member;
};
int main() {
/*referans karşılaştırma değeri*/
int reference = 10;
Functor f_obj(reference);
bool result = f_obj(11, 0);
if (result) {
cout << "Ok" << endl;
}
else {
cout << "Not Ok" << endl;
}
return 0;
}
Örneği functor.cpp ismiyle saklayıp aşağıdaki gibi derleyebilirsiniz.
g++ -ofunctor functor.cpp
Örnekte temel olarak verilen 2 sayı arasındaki farkın bir referans değerden büyük olup olmadığına bakılmıştır. Yapılan işlemleri daha yakından inceleyelim.
1. Functor f_obj(reference);
2. bool result = f_obj(11, 0);
1.
satırda sınıfın başlangıç fonksiyonuna (constructor) referans değeri geçirilerek f_obj nesnesi yapılandırılmış, 2.
satırda ise f_obj nesnesi üzerinden sınıfın operator() üye fonksiyonu çağrılarak karşılaştırma işlemi yapılmıştır.
Sınıfın başlangıç fonksiyonuna geçirilen referans değeri, sınıfın m_member üye değişkeninin değerini değiştirerek bir durum (state) bilgisini oluşturmaktadır. Daha sonra yapılacak olan karşılaştırma işleminde bu durum bilgisi kullanılmaktadır. Yukarıdaki örnek için bu durum bilgisi görsel olarak aşağıdaki gibi gösterilebilir.
m_member sınıfın data belleğindeki görüntüsünü oluşturmaktadır. operator() üye fonksiyonuna m_member üye değişkeninin adresi ilk argüman olarak geçirilmektedir.
Not: Üye fonksiyonlara ilk argüman olarak üzerinde işlem yapacakları nesnenin adresinin gizli bir biçimde geçirildiğini hatırlayınız. Bu adrese üye fonksiyon içerisinde this anahtar sözcüğü ile ulaşmaktayız.
Burada m_member değişkeni durum bilgini tutmakta ve operator() fonksiyonunun davranışını değiştirmektedir.
Bölümün başında anonim fonksiyonlar için derleyicinin bir tür yazdığını, bu tür için bir operator() üye fonksiyonu tanımladığını ve işlemlerin bu türden oluşturulan isimsiz bir nesne üzerinden yapıldığını söylemiştik. Derleyicinin anonim fonksiyonlar için nasıl bir kod yazdığını ve anonim fonksiyonların fonksiyon nesneleriyle olan ilişkisini görmek için aşağıdaki örnek kodu inceleyelim.
#include <iostream>
using namespace std;
#ifdef FUNCTOR
class Functor {
public:
#ifdef INLINE
Functor(int& total) __attribute__((always_inline))
: m_total(total) {}
#else
Functor(int& total)
: m_total(total) {}
#endif
void operator()(int num) {
m_total += num;
}
private:
int& m_total;
};
#endif
int main()
{
int total = 0;
#ifdef FUNCTOR
Functor fobj(total);
fobj(111);
printf("%d\n", total);
#else
[&total] (int num) { total += num; } (111);
printf("%d\n", total);
#endif
return 0;
}
main içinde aynı sonucu üreten, önişlemci direktifleriyle ayrılmış, iki adet kod bloğu bulunmaktadır. Derleme işlemine hangi bloğun girecegine FUNCTOR makrosunun varlığına göre karar verilmektedir. INLINE makrosunu ne amaçla kullandığımızı daha sonra söyleyeceğiz.
Not: Derleyiciye komut satırında -D anahtarı geçirerek bir makro tanımlaması sağlanabilmektedir.
Her iki kod bloğunda da main fonksiyonunun yerel değişkeninin değeri başka bir fonksiyon tarafından değiştirilmektedir. İlk olarak bu işlemin bizim yazdığımız bir sınıfa ait fonksiyon nesnesiyle nasıl yapıldığını, sonrasında ise bir anonim fonksiyon kullanılarak nasıl yapıldığını inceleyeceğiz.
Fonksiyon Nesnesinin Açık Kullanımı
Örnek uygulamaya lambda.cpp ismini verdikten sonra aşağıdaki gibi derleyebiliriz.
g++ -olambda lambda.cpp -m32 -std=c++11 -DFUNCTOR --save-temps
Not: -std anahtarı ile derleyiciye kullanmasını istediğimiz standardı belirtiyoruz.
lambda.s dosyasını adım adım inceleyerek işe başlayalım. Derleyicinin main fonksiyonu için aşağıdaki gibi bir kod ürettiğini görmekteyiz.
main: pushl %ebp movl %esp, %ebp andl $-16, %esp subl $32, %esp movl $0, 24(%esp) leal 24(%esp), %eax movl %eax, 4(%esp) leal 28(%esp), %eax movl %eax, (%esp) call _ZN7FunctorC1ERi movl $111, 4(%esp) leal 28(%esp), %eax movl %eax, (%esp) call _ZN7FunctorclEi movl 24(%esp), %eax movl %eax, 4(%esp) movl $.LC0, (%esp) call printf movl $0, %eax leave ret
main için yığın alanından 32 byte'lık yer ayrılmış.
subl $32, %esp
Yığının tepe noktasına 24 byte uzaklıktaki alan total yerel değişkeni için ayrılmış ve bu alana 0 değeri atanmış.
movl $0, 24(%esp)
Yerel değişkenin adresi ilk önce eax yazmacına yazılmış ve oradan yığının tepe noktasına 4 byte uzaklıktaki alana kopyalanmış.
leal 24(%esp), %eax
movl %eax, 4(%esp)
Yığının tepe noktasına 28 byte uzaklıktaki alanın adresi önce eax yazmacına yazılmış ve oradan yığının tepe noktasından başlayan alana yazılmış.
leal 28(%esp), %eax
movl %eax, (%esp)
Sonrasında aşağıdaki gibi bir fonksiyon çağrısına ilişkin sembolik makina kodunu görmekteyiz.
call _ZN7FunctorC1ERi
C++ derleyicisinin fonksiyon isimlerini dekore ettiğini hatırlayınız. C++ derleyicisi ürettiği sembolik makina kodunda, kullanıcının tanımladığı isimleri değil, kendi ürettiği aşağı seviyeli assembler isimlerini kullanmaktadır.
Not: binutils paketinden çıkan c++filt aracı ile dekore edilmiş isimler kullanıcının tanımladığı isimlere geri dönüştürülebilir.
c++filt ile çağrı yapılan sembolün hangi fonksiyona ait olduğunu bulabiliriz.
$ c++filt _ZN7FunctorC1ERi Functor::Functor(int&)
c++filt çıktısından buradaki çağrının sınıfın başlangıç fonksiyonuna (constructor) ait olduğunu görüyoruz. Bu aşamada başlangıç fonksiyonu çağrıldığında yığının durumu aşağıdaki gibidir.
Başlangıç fonksiyonuna ilk argüman olarak yapılandıracağı nesnenin, ikinci argüman olarak ise yerel total değişkeninin adresi geçirilmektedir. C++ kodunda, yerel değişken adresinin referans yoluyla gizli bir biçimde geçirildiğine dikkat ediniz. Bu durumda yığının tepesinde güvenli alan adresi olarak gösterdiğimiz alandaki adres fonksiyon nesnesi için kullanılacak alanı göstermektedir.
Not: gcc derleyicisi, C++ dilinde sınıfın statik olmayan üye fonksiyonları için thiscall çağırma biçimini (calling convention) kullanmaktadır. thiscall çağırma biçimi C dilinde cdecl çağırma biçimine oldukça benzemektedir. Çağırılan fonksiyonlara argümanlar yığın yoluyla geçirilmekte ve sağdan sola doğru yığına atılmaktadır. Bu durumda yığının tepesindeki değer çağırılan fonksiyonun en soldaki yani ilk parametresine denk gelmektedir. thiscall çağırma biçiminde cdecl çağırma biçiminden farklı olarak yığının en tepesi gizli bir this göstericisi geçirilmektedir.
Derleyicinin sınıfın başlangıç kodu için ürettiği sembolik makina kodu ise aşağıdaki gibidir.
_ZN7FunctorC2ERi: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax movl 12(%ebp), %edx movl %edx, (%eax) popl %ebp ret
Not: Başlangıç fonksiyonu main içinde _ZN7FunctorC1ERi adıyla çağırılmasına karşın fonksiyon tanımı _ZN7FunctorC2ERi şeklinde yapılmış. Nedeni konumuzun dışında olduğundan yalnız bu detayı söyleyip geçeceğiz.
Başlangıç fonksiyonuna geçirilen ilk argüman (nesnenin adresi) eax yazmacına, ikinci argüman (yerel değişken adresi) ise edx yazmacına yazılmış.
movl 8(%ebp), %eax
movl 12(%ebp), %edx
edx yazmacındaki yerel değişken adresi, eax yazmacının bellekte gösterdiği alana yazılmış.
movl %edx, (%eax)
Bu andan itibaren, fonksiyon nesnesi yapılandırılmış ve m_total üye değişkeni main fonksiyonunun yerel değişkeninin adresini tutar durumdadır. Başlangıç fonksiyonu döndüğünde yığının durumu aşağıdaki gibidir.
Tekrar main fonksiyonuna döndüğümüzde, 111 değerinin ve m_total üye değişkeninin adresinin sırasıyla yığına atıldığını görüyoruz. m_total değişkeni fonksiyon nesnesinin data belleğinde kapladığı alanı göstermektedir.
movl $111, 4(%esp)
leal 28(%esp), %eax
movl %eax, (%esp)
Sonrasında aşağıdaki fonksiyon çağrısını görmekteyiz.
call _ZN7FunctorclEi
Dekore edilmiş sembol adına c++filt ile baktığımızda sınıfın operator() fonksiyonuna ait olduğunu görmekteyiz.
$ c++filt _ZN7FunctorclEi Functor::operator()(int)
operator() fonksiyonuna ait sembolik makina kodu aşağıdaki gibidir.
_ZN7FunctorclEi: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax movl (%eax), %eax movl 8(%ebp), %edx movl (%edx), %edx movl (%edx), %ecx movl 12(%ebp), %edx addl %ecx, %edx movl %edx, (%eax) popl %ebp ret
Makina kodlarına yakından bakalım. Fonksiyona geçirilen ilk argüman değeri ilk önce eax yazmacına yazılmış, ardından yazmacın gösterdiği adrese karşılık gelen bellek alanındaki değer tekrar eax yazmacına kopyalanmış.
movl 8(%ebp), %eax
movl (%eax), %eax
Bu işlem C dilinden aşina olduğumuz pointer dereference işlemine karşılık gelmektedir. Son durumda eax yazmacında m_total değişkeninin değeri yani main fonksiyonunun yerel değişkeninin (total) adresi bulunmaktadır. Sonraki üç komut ile iki defa dereference işlemi yapılarak ecx yazmacına total yerel değişkeninin değeri yazılmış.
movl 8(%ebp), %edx
movl (%edx), %edx
movl (%edx), %ecx
Son durumda, eax yazmacında total yerel değişkeninin adresi, ecx yazmacında ise değeri bulunmaktadır. Daha sonra operator() fonksiyonuna açık olarak geçirilen argüman, örneğimiz için 111, ilk önce edx yazmacına atılmış, total yerel değişkeninin değeriyle toplanarak yerel değişkenin bellek alanına yazılmış.
movl 12(%ebp), %edx
addl %ecx, %edx
movl %edx, (%eax)
main fonksiyonuna geri döndüğümüzde geri kalan komutların yerel değişkenin değerinin standart çıktıya basılmasıyla ilgili olduğunu görmekteyiz.
Özetleyecek olursak, main fonksiyonunun yerel değişkeninin adresi bir fonksiyon nesnesinde durum bilgisi olarak saklanmış ve operator() fonksiyonuyla bu adrese ulaşılarak yerel değişkenin değeri değiştirilmiştir.
Lamdba İfadeleri
Daha önce derleyicinin lambda ifadelerini kullanarak bizim için bir tür yazdığından bahsetmiştik. Şimdi bu duruma daha yakından bakalım. Bir önceki konu başlığında incelediğimiz örnek kodu FUNCTOR makrosu tanımlamaksızın aşağıdaki gibi derleyelim.
g++ -olambda lambda.cpp -m32 -std=c++11 --save-temps
Bu durumda anonim fonksiyon çağrısı derleme sürecine girecektir. Anonim fonksiyonun tanımlandıktan hemen sonra çağrıldığına dikkat ediniz.
[&total] (int num) { total += num; } (111);
lambda ifadesinin genel formunu yeniden hatırlatarak daha yakından bakalım.
[capture-list] (parameters) -> return_type {function_body}
Köşeli parantezler boş bırakılabildiği gibi dışsal değişkenler virgül ile ayrılmış bir liste şeklinde geçirilebilir. Bu dışsal değişkenlere değer (capture by value) veya adres (capture by reference) yoluyla erişilebilir. Örnek bazı kullanımlar aşağıdaki gibi verilebilir.
Kullanım | Açıklama |
---|---|
[ ] |
Dışsal bir değişkene erişim yoktur |
[&] |
Bütün dışsal değişkenlere adres ile erişilir |
[=] |
Bütün dışsal değişkenlere değer ile erişilir |
[x, &y] |
x değişkenine değerle y değişkenine adres ile erişilir |
[&, x] |
x değişkenine değer ile erişilirken diğer tüm dışsal değişkenlere adres ile erişilir |
Burada dışsal değişken ile anonim fonksiyonun içinde tanımlandığı fonksiyona ait yerel değişkenleri kastettiğimizi hatırlatalım.
Köşeli parantezlerden sonra parametre değişkenleri ve fonksiyon gövdesi yazılır. Çoğu durumda geri dönüş değerinin türü derleyici tarafından fonksiyon gövdesine bakılarak tahmin edilmektedir. Buna karşın geri dönüş türü açık bir şekilde de yazılabilir.
Not: Aslında bir lambda ifadesinin en genel formu aşağıdaki gibidir.
[ capture-list ] ( params ) mutable(optional) exception attribute -> ret { body }
Biz burada genel olarak anonim fonksiyonların işleyişiyle ilgilendiğimizden detaya girmeyeceğiz.
Köşeli parantezler içinde geçirdiğimiz dışsal değişkenler fonksiyon gövdesi içinde kullanılabilmektedir. Tekrardan örnek koddaki lambda ifadesine baktığımızda total yerel değişkenine adres yoluyla erişildiğini ve fonksiyon gövdesinde sol taraf değeri olarak kullanıldığını görmekteyiz.
Derlediğimiz kodu çalıştırdığımızda bir öncekiyle aynı sonucu ürettiğini göreceğiz.
Şimdi derleyicinin anonim fonksiyon için ürettiği kodu bir önceki bölümde incelediğimiz kod ile karşılaştırarak inceleyelim. Bir önceki bölümde bir functor sınıfı yazmış ve yerel değişkenin değerini bu sınıftan oluşturduğumuz nesne ile değiştirmiştik.
Her iki kodda da yığının tepe noktasından itibaren 24 byte uzaklıktaki alan total yerel değişkeni için ayrılmış ve 0 ilk değeri verilmiş.
movl $0, 24(%esp)
Functor örneğine baktığımızda bundan sonraki 4 sembolik makina komutunun Functor sınıfının başlangıç fonksiyonuna geçirilecek argümanlarla ilgili olduğunu görmekteyiz. Yerel değişkenin ve nesne için ayrılmış alanın adresleri sırasıyla yığına atılmış.
leal 24(%esp), %eax
movl %eax, 4(%esp)
leal 28(%esp), %eax
movl %eax, (%esp)
Sonrasında sınıfın başlangıç kodu çağrılarak nesne için ayrılan alana yerel değişkenin adresi yazılmış. Nesne için yığının başından itibaren 28 byte uzaklıktaki alanın ayrıldığına dikkat ediniz. Bu aşamada anonim fonksiyon örneğine baktığımızda aynı işlemin aşağıdaki gibi yapıldığını görmekteyiz.
leal 24(%esp), %eax
movl %eax, 28(%esp)
Gerçekten de functor örneğinde başlangıç fonksiyonunu inline olarak tanımladığımızda, derleyici bir fonksiyon çağrısı yapmak yerine, buradaki kodun aynısını üretecektir. Bunun için bir önceki örnekte derleyiciye -DINLINE argümanı geçirerek bu durumu inceleyebilirsiniz.
Sonrasında her iki kod örneğinde de 111 değeri ve yerel değişkenin adresini tutan alanın (fonksiyon nesnesi) adresi yığına aktarılmış ve ardından fonksiyon çağrıları yapılmış. Functor örneği için yapılan çağrının sınıfın operator() üye fonksiyonuna olduğunu hatırlayınız. Anonim fonksiyon örneğinde ise çağrı aşağıdaki gibidir.
call _ZZ4mainENKUliE_clEi
c++filt ile sembolün kullanıcı seviyesindeki karşılığına baktığımızda şöyle bir sonuç ürettiğini görmekteyiz.
$ c++filt _ZZ4mainENKUliE_clEi main::{lambda(int)#1}::operator()(int) const
Buradan derleyicinin bizim için const bir operator() fonksiyonu yazdığını ve çağırdığını anlayabiliriz.
main::{lambda(int)#1} bize derleyicinin bizim için yazdığı tür adını göstermektedir. lambda ifadesinin main fonksiyonu içinde yazıldığını ve int türden parametreye sahip olduğunu hatırlayınız. Derleyicinin yazdığı operator() fonksiyonuna baktığımızda daha önce bizim yazdığımız operator() fonksiyonuyla aynı olduğunu görmekteyiz.
Burada derleyici, bizim yazdığımız lambda ifadesinden yola çıkarak, yerel değişkenin adresini tutan bir fonksiyon nesnesi oluşturmuş, ardından operator() fonksiyonu içinde bu yerel değişken adresini ve kullanıcının geçirdiği değeri kullanmış. Daha önce de söylediğimiz gibi burada yerel değişkenin adresini tutan isimsiz nesne closure olarak isimlendirilir. İsimsiz fonksiyon nesnesinin otomatik ömürlü olduğuna yani yığında oluşturulduğuna dikkat ediniz.
Bu aşamada anonim fonksiyonların kullanımına birkaç örnek vermek yararlı olacaktır. Anonim fonksiyonlar, şablonlarla (template) yoğun bir kullanıma sahip, fonksiyon nesneleri yerine kullanılabilir. Aşağıdaki örneği inceleyiniz.
#include <iostream>
#include <vector>
using namespace std;
#ifndef LAMBDA
class AccumulatorFunctor {
public:
AccumulatorFunctor(int& total)
: m_total(total) {}
void operator()(int num) {
if (num % 2 == 0) {
m_total += num;
}
}
private:
int& m_total;
};
#endif
template<class InputIt, class UnaryFunction>
UnaryFunction for_each(InputIt first, InputIt last, UnaryFunction f)
{
for (; first != last; ++first) {
f(*first);
}
return f;
}
int main() {
vector<int> v = {1, 2, 3, 4, 5, 6};
int total = 0;
#ifndef LAMBDA
for_each(v.begin(), v.end(), AccumulatorFunctor(total));
#else
for_each(v.begin(), v.end(), [&total] (int num) { total += (num % 2 == 0) ? num : 0; });
#endif
cout << "total: " << total << endl;
return 0;
}
Örnekte, bir vektördeki çift sayıların toplamının yerel total değişkenine yazılması hedeflenmiş. Kod içerisinde aynı işin, hem bir fonksiyon nesnesiyle hem de anonim fonksiyon ile nasıl yapıldığının örneği bulunmaktadır. lambda ifadesi kullanılarak oluşturulan isimsiz fonksiyon nesnesi (closure) fonksiyon şablonu (function template) kullanılarak yazılan for_each fonksiyonuna callback olarak geçirilmiş. İşlemin anonim fonksiyon ile gerçekleştirilmesi için kodu aşağıdaki gibi derleyebilirsiniz.
g++ -Wall -olambda lambda.cpp -m32 --save-temps -std=c++11 -DLAMBDA
Ayrıca, anonim fonksiyonlar herhangi bir dışsal değişkenle ilişkilendirilmediği durumda ([] içinin boş olduğu durum) gizli bir biçimde (implicitly) fonksiyon göstericisine dönüştürülerek callback olarak kullanılabilir. Aşağıdaki örneği inceleyiniz.
#include <iostream>
#include <vector>
using namespace std;
typedef int (*PF) (int);
void foo(PF f) {
int result = f(111);
if (result) {
cout << "Odd" << endl;
}
else {
cout << "Even" << endl;
}
}
int main() {
int total;
(void)total;
foo([] (int arg) { return (arg & 1); });
return 0;
}
Örnekte total yerel değişkeninin anonim fonksiyon içinde kullanılmadığına dikkat ediniz. Dışarıya geçirilen içsel bir fonksiyon ile yerel bir değişkene ulaşabilmek için GNU C eklentilerince yığında bir trambolin kodu yazıldığını hatırlayınız. Daha önce de belirttiğimiz gibi GNU C++ ise bu eklentiyi içermemektedir.
Son olarak kısaca C++ diline eklenen anonim fonksiyon ya da closure kavramını, diğer bazı dillerdeki yakın kullanımlarıyla karşılaştıracağız. Buradaki closure ifadesi, Java diline Java 8 ile eklenen ve javascript dilinde kullanılmakta olan closure ile tam olarak aynı anlamı taşımamaktadır. Daha sınırlı bir kullanıma sahiptir.
Daha önce içsel fonksiyonların dışarıdan asenkron çağrılmaları durumunda belirsiz davranış oluşacağından bahsetmiştik. Aynı problem burada anonim fonksiyonlar için de geçerlidir. Anonim fonksiyonun içinde tanımlandığı fonksiyon sonlandığında bu foksiyona ait yığın alanı geri verilmekte ve sonraki anonim fonksiyon çağrıları güvenilir olmayan bir alan üzerinde işlem yapmaktadır. Bu durumu, otomatik ömürlü yerel bir değişken adresini dönen bir fonksiyonun geri dönüş değerinin kullanımına benzetebiliriz. Java ve javascript gibi dillerde ise içinde anonim fonksiyon tanımlanan fonksiyonlara ait yığın alanı bir şekilde saklanmaktadır. C++ dilinde birçok yönden kullanışlı olan bu özellik maalesef şu haliyle asenkron olarak gerçekleşen bir olayı dinlemek için uygun değildir.
Derleyicinin anonim fonksiyonları nasıl ele aldığını bilmek, bizim bu özelliğin sınırlarını bilerek daha doğru kullanmamıza yardımcı olacaktır.