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.
Örnek bir anonim fonksiyon tanımı ise aşağıdaki gibidir.
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.
Örneği functor.cpp ismiyle saklayıp aşağıdaki gibi derleyebilirsiniz.
Ö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.
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.
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.
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 için yığın alanından 32 byte'lık yer ayrılmış.
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ış.
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ış.
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ış.
Sonrasında aşağıdaki gibi bir fonksiyon çağrısına ilişkin sembolik makina kodunu görmekteyiz.
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 çı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.
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ış.
edx yazmacındaki yerel değişken adresi, eax yazmacının bellekte gösterdiği alana yazılmış.
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.
Sonrasında aşağıdaki fonksiyon çağrısını görmekteyiz.
Dekore edilmiş sembol adına c++filt ile baktığımızda sınıfın operator() fonksiyonuna ait olduğunu görmekteyiz.
operator() fonksiyonuna ait sembolik makina kodu aşağıdaki gibidir.
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ış.
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ış.
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ış.
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.
Bu durumda anonim fonksiyon çağrısı derleme sürecine girecektir. Anonim fonksiyonun tanımlandıktan hemen sonra çağrıldığına dikkat ediniz.
lambda ifadesinin genel formunu yeniden hatırlatarak daha yakından bakalım.
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 değişkenine değerle y değişkenine adres ile erişilir |
| 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ş.
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ış.
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.
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.
c++filt ile sembolün kullanıcı seviyesindeki karşılığına baktığımızda şöyle bir sonuç ürettiğini görmekteyiz.
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.
Ö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.
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.
Ö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.
Last updated