lambda ifadeleri (lambda expressions)

Bu yazımızla C++11 standartları ile dile eklenen ve C++ dilinin en sık kullanılan araçlarından biri haline gelen lambda ifadelerini ele alacağız.
Bir lambda ifadesi çağrılabilecek (callable) bir kod birimini temsil eder. lambda ifadesi karşılığında, derleyici kendi oluşturduğu bir sınıf türünden bir geçici nesne oluşturacak bir kod üretir. Örneğin

gibi bir ifadenin derleyici tarafından

ifadesine dönüştürüldüğünü ve aclass isminin de derleyicinin arka planda oluşturduğu, aşağıdaki gibi bir sınıf olduğunu düşünebilirsiniz:

lambda ifadeleri programcıya, bir işlev çağrısının yapılacağı yerde doğrudan işlevin kodunu yazabilme olanağını sağlar. Yani lambda ifadeleriyle isimlendirilmemiş (anonim) ve inline bir işlevin kodunu derleyiciye yazdırmış oluruz.
lambda ifadesinde yer alan

derleyicinin oluşturduğu sınıfın işlev çağrı operatörü işlevinin parametre değişkenleri olur.
Yine lambda ifadesindeki küme parantezleri içindeki kod

derleyicinin oluşturduğu sınıfın işlev çağrı operatörü işlevinin kodudur.
C++ dilinin kurallarına göre bir işlev içinde başka bir işlev tanımının bulunması geçerli değildir. Ancak lambda ifadeleri bir işlev tanımı içinde kullanılabilirler.

Derleyicinin lambda ifadesi karşılığı oluşturduğu sınıf, ingilizcede “closure type”, lambda ifadesi ile oluşturulan nesne de “closure” ya da “closure object” olarak isimlendirilir.

lambda ifadeleri ile oluşturulan geçici nesneler farklı amaçlarla kullanılabilirler. Ancak lambda ifadelerinin en çok işlev şablonlarına gönderilen argüman olarak kullanılırlar:

Yukarıdaki main işlevinde ivec isimli vector‘de kaç öğenin ival değişkeninin değerine tam olarak bölündüğü count_if algoritmasıyla sayılıyor. count_if algoritmasına yapılan çağrıda işlevin 3. parametresine argüman olarak

ifadesinin gönderildiğini görüyorsunuz. Derleyiciye göre bu ifade, derleyicinin bu lambda ifadesi karşılığı oluşturduğu geçici nesnenin türündendir. Derleyici count_if işlev şablonundan hareketle, yazdığı işlevin üçüncü parametresinin lambda ifadesinin türünden olduğu çıkarımını yapar. Eğer lambda ifadeleri olmasaydı böyle bir sınıfı kendimizin oluşturacak ve algoritmaya kendi oluşturduğumuz geçici nesneyi göndermemiz gerekecekti. Aşağıdaki koda bakalım:

temel lambda sentaksı

lambda sentaksı başlangıçta programcıya biraz karışık gelebilir. C++14 ve C++17 standartlarıyla lambda ifadelerine yeni özellikler de getirildiğini belirtelim. Bu yüzden önce ayrıntıları çok dikkate almadan genel sentaks üzerine odaklanacak, ayrıntıları ileride ele alacağız. En genel haliyle bir lambda ifadesi şöyledir:

Sentaksın temel bileşenlerinden parametre listesi (lambda declarator) ile başlayalım. Parantez ( ) içinde tanımladığımız değişkenler derleyicinin yazacağı işlevin parametre değişkenleridir:

Yukarıdaki kodda kullanılan lambda ifadesi için derleyicinin şöyle bir sınıf kodu yazdığını düşünebiliriz:

lambda ifadesi karşılığı oluşturulacak işlevin parametre değişkeni yok ise parametre parantezi yazılmayabilir. Aşağıdaki koda bakınız:

Yukarıdaki kodda auto belirteci kullanılarak p değişkenine lambda ifadesi ile oluşturulan geçici nesne ile ilk değer veriliyor. Bu durumda p değişkeni derleyicinin lambda ifadesi karşılığında oluşturacağı sınıfın türündendir. p değişkeni işlev çağrı işlecinin terimi (operand) olduğundan, çağrılan bu sınıfın işlev çağrı operatör işlevi olur. İşlev çağrısı oluşturulan geçici nesne ile doğrudan da yapılabilirdi:

lambda işlevlerin geri dönüş türleri

Aksi yönde bir belirleme yapılmadıkça, derleyici bir lambda işlevinin geri dönüş değerinin türünü lambda ifadesinde kullanılan return ifadesinin türü kabul eder. Yani derleyici lambda işlevinin geri dönüş değerinin türünün ne olduğunu return ifadesinden bir çıkarım yaparak anlar. Örneğin

ifadesiyle oluşturulacak lambda işlevinin geri dönüş değerinin türü

ifadesinin türü olan bool‘dur.

ifadesiyle oluşturulacak lambda işlevinin geri dönüş değerinin türü

ifadesinin türü olan int’tir.

lambda ifadesinde yer alan blok içinde yazılan kodda bir return deyimi yok ise lambda işlevin geri dönüş değerinin türü void kabul edilir:

Yukarıdaki lambda ifadesine ilişkin derleyicinin yazacağı işlev void geri dönüş değeri türüne sahiptir.

lambda
 işlevin geri dönüş değerinin türü açıkça da belirtilebilir. Bu durumda işlevin geri dönüş değerinin türü kullanılan return ifadesinin türü olmak zorunda değildir:

lambda işlevinin geri dönüş değerinin türünün parametre parantezini izleyen ok (->) atomundan sonra yazıldığını (trailing return type) görüyorsunuz. Derleyicinin bu lambda ifadesi karşılığı oluşturacağı lambda işlevinin geri dönüş değeri double türden olur.

Eğer işlevin kodu tek bir return deyiminden oluşmuyor ise geri dönüş değeri türünün açıkça yazılması zorunludur. Aksi halde işlevin geri dönüş değeri türü void kabul edilir. Aşağıdaki koda bakalım:

Yukarıdaki kodda yer alan lambda ifadesinde lambda işlevin ana bloğunun içinde iki ayrı return deyimi yazılmış. Bu durumda işlevin geri dönüş değeri void türü kabul edilecek. İşlev geri dönüş değeri ürettiğinden kod geçerli değildir. İşlevin geri dönüş değerinin türü açıkça belirtilmeliydi:

İşlevin kodu tek bir return deyiminden oluşsaydı geri dönüş değerinin türü derleyicinin çıkarımına bırakılabilirdi:

Yukarıdaki deyimde kullanılan lambda ifadesine ilişkin yazılacak işlevin geri dönüş değeri, return ifadesinin türü olan int türündendir.

yakalama listesi (lambda introducer – lambda clause)

Normal olarak bir işlev yalnızca kendi parametre değişkenlerini ve yerel değişkenlerini kullanabilir. Ancak bir lambda ifadesi içinde, o ifadeyi kapsayan bloklarda yer alan dışsal değişkenler özel bir sentaks ile kullanılabilir. Kapsayan bloktaki bir nesne, kopyalama ya da referans semantiği ile lambda ifadesi içinde kullanılabilir:

main işlevi içinde kullanılan lambda ifadesine bakalım:

[ ] içine, yani yakalama listesine yazılan n, kapsayan blok içinde yer alan n değişkeninin lambda ifadesi tarafından kopyalama yoluyla yakalandığını gösteriyor. Böylece n değişkeni lambda işlevinin kodu içinde kullanılabilir. Derleyicinin yukarıdaki lambda ifadesi karşılığı aşağıdaki gibi bir sınıfın kodunu yazdığını düşünebilirsiniz:

lambda ifadesinin kullanıldığı yerde ise derleyici lambda ifadesini

gibi bir ifadeye dönüştürüyor ve böylece biz de xyz_ sınıfı türünden geçici bir nesne oluşturmuş oluyoruz.
Eğer kapsayan bir blok içinde yer alan bir nesnenin kopyası değil de kendisi kullanılmak istenirse yakalama (capture) referans semantiği ile yapılmalıdır. Aşağıdaki kodu inceleyelim:

Yukarıdaki kodda kullanılan lambda ifadesinin yakalama listesinin

biçiminde yazıldığını görüyorsunuz. Bu, kapsayan bloktaki a değişkeninin referans yoluyla yakalanacağını anlatıyor. Yani bu durumda lambda işlevi içinde a ismini kullandığımızda a değişkenin bir kopyasını değil de kendisini kullanmış oluyoruz. Derleyicinin böyle bir lambda ifadesi karşılığı aşağıdaki gibi bir sınıfın kodunu yazdığını düşünebilirsiniz:

Kopyalama ya da referans yoluyla yakalama arasındaki farkı daha iyi anlayabilmek için aşağıdaki kod örneğine bakalım:

main işlevi içinde oluşturulan lambda ifadesi yerel x değişkenin kopyalama yoluyla yakalıyor. lambda işlevin geri dönüş değeri deyiminin

olduğunu görüyorsunuz. İşlev main içinde tanımlanan x değişkenin değerini değil, bu değişken ile ilk değerini alan, sınıfın ilgili veri öğesinin değerini döndürüyor. lambda işlevi çağrılmadan önce, yerel x değişkenine 0 değeri atanıyor. Kodu çalıştırdığınızda çağrılan lambda işlevinin 0 değerini değil 10 değerini döndürdüğünü göreceksiniz. Eğer yakalama kopyalama yoluyla değil de referans yoluyla yapılsaydı çağrılan lambda işlevinin geri dönüş değeri 0 olacaktı:

Yakalama listesi aşağıdaki biçimlerden birine uygun olmalıdır:

[ ] boş yakalama listesi

Eğer bir lambda ifadesinin yakalama listesi boş ise, yani [ ] içi boş bırakılırsa bu lambda kapsayan kod alanındaki bir değişkene erişmiyor yani yalnızca kendi parametrelerini ve yerel değişkenlerini kullanıyor demektir.

lambda ifadesi ile hiç bir dışsal isim yakalanmıyor olsa dahi yakalama listesinin yazılması zorunludur. Yani tüm lambda ifadelerinde [ ] bulunmak zorundadır.

[=] kopyalama yoluyla yakalama

Bu durumda kullanılan tüm dış isimler varsayılan şekilde kopyalama ile yakalanır. Her bir değişkenin isminin tek tek yazılması zorunlu değildir.

[&] referans yoluyla yakalama

Bu durumda kullanılan tüm dış isimler referans yoluyla yakalanır. Yine her bir değişkenin isminin tek tek yazılması zorunlu değildir.

Yukarıdaki lambda ifadesinde kapsayan blokta yer alan a, b ve c değişkenleri referans yoluyla yakalanıyor. Yani lambda işlev içinde a, b ve c değişkenlerinin kendileri kullanılıyor. Şimdi de aşağıdaki yakalama listesine bakalım:

Buradaki = karakteri varsayılan yakalama biçiminin kopyalama olduğunu anlatıyor. Bu, x dışında kullanılan tüm isimlerin kopyalama yoluyla yakalanacağı yalnızca x değişkeninin referans yoluyla yakalanacağı anlamına geliyor.

Buradaki & karakteri varsayılan yakalama biçiminin referans yoluyla olduğunu anlatıyor. Bu durumda, y dışında kullanılan tüm isimler referans yoluyla, yalnızca y değişkeni kopyalama yoluyla yakalanacak.
Yakalama listesinde yakalamanın hangi biçimle yapılacağı her bir değişken için ayrı ayrı gösterilebilir:

Yukarıdaki yakalama listesine göre, a ve c değişkenleri kopyalama yoluyla, b ve d değişkenleri referans yoluyla yakalanacak. lambda ifadesini kapsayan kod alanından, yalnızca a, b, c, d değişkenleri lambda işlevin ana bloğu içinde kullanılabilir.

Kapsayan bloklardaki statik ömürlü yerel değişkenler ve global isim alanında görünür durumda olan global değişkenler (yani statik ömürlü tüm değişkenler) yakalama listesinde belirtilmeden doğrudan kullanılabilirler:

this adresinin yakalanması

Bir sınıfın static olmayan bir işlevi içinde bir lambda ifadesi oluşturduğumuzu düşünelim. Bu lambda ifadesi sınıfın static olmayan veri öğelerini yakalayabilir mi? Aşağıdaki kodu inceleyelim:

Yukarıdaki kodda Myclass sınıfının func isimli üye işlevi içinde kullanılan lambda ifadesinin sınıfın mx isimli veri öğesini yakalamaya çalıştığını görüyorsunuz. Bu kod geçerli değildir. mx, lambda ifadesini kapsayan kod alanında olmadığı için yakalanamaz. Ancak this göstericisi lambda ifadesini kapsayan kod alanında kabul edildiği için yakalanabilir ve böylece lambda ifadesi yakaladığı this göstericisi yoluyla sınıfın static olmayan veri öğelerini kullanabilir. lambda ifadesi şu şekilde yazılsaydı geçerli olurdu:

mutable lambdalar

Bir lambda işlevi kopyalama yoluyla yakaladığı değişkenleri değiştiremez. Çünkü kopyalama yoluyla yakalanan değişkenler aslında derleyicinin oluşturduğu sınıfın ilgili veri elemanına ilk değer verirler. Derleyicinin kodunu yazdığı çağrı operatörü işlevi varsayılan şekilde const bir üye işlevdir. Aşağıdaki ifadenin neden geçersiz olduğunu anlamaya çalışalım:

Sınıfın const üye işlevlerinin sınıfın statik olmayan veri öğelerini (non static data members) değiştiremediğini hatırlayalım. Yukarıdaki lambda ifadesi karşılığı derleyicinin yazacağı sınıfın kodunun şöyle olduğunu düşünelim:

Derleyicinin yazdığı sınıfın const olan operator() işlevinin, sınıfın veri öğelerini değiştirebilmesi için, bu veri öğelerinin mutable anahtar sözcüğüyle bildirilmeleri gerekir, değil mi? Bunu sağlamak için lambda ifadesinde mutable anahtar sözcüğü kullanılıyor:

mutable anahtar sözüğünün lambda ifadesi içinde kullanılması durumunda lambda işlevin parametre parantezinin, hiçbir parametre değişkeni olmasa da, mutlaka yazılması gerekir.

lambda ifadeleri ve işlev göstericileri

Bir lambda ifadesi eğer kapsayan kod alanındaki yerel bir değişkeni yakalamıyorsa (non capturing lambda) derleyici lambda ifadesi için doğrudan bir işlevin kodunu inline olarak yazabilir. Bu durumda bir işlev adresi gereken yerde bir lambda ifadesi kullanıldığında, derleyicinin oluşturduğu geçici nesne otomatik olarak lambda işlevinin adresine dönüştürülür. Aşağıdaki kodu inceleyelim:

Global func işlevinin birinci parametresinin fp isimli bir işlev göstericisi olduğunu görüyorsunuz. İşlevin kodunda fp işlev göstericisinin gösterdiği işlev ikinci parametre olan x‘i argüman olarak alarak çağrılıyor. main işlevi içinde ise lambda ifadesi karşılığında oluşturulan nesne, func işlevinin birinci parametresine gönderilerek geri çağrı (callback) mekanizmasından faydalanılıyor.
lambda ifadesinin dışsal bir ismi yakalaması durumunda (capturing lambda) derleyici lambda ifadesi karşılığı bir sınıf kodu yazar ve bu sınıf türünden geçici bir nesne oluşturur. Oluşturulan bu nesne otomatik olarak bir işlev adresine dönüştürülemez. Bir işlevin geri çağrı mekanizmasıyla böyle bir lambda işlevini kullanabilmesi için işlevin parametresi std::function türünden yapılmalıdır:

Yukarıdaki kodda func işlevinin birinci parametresinin türünden olduğunu görüyorsunuz. Bu durumda func işlevinin birinci paremetresine argüman olarak, geri dönüş değeri int parametresi int olan herhangi bir çağrılabilir birim (callable) gönderilebilir.

lambda ifadelerinin türleri

lambda ifadeleri tamamen aynı olsa dahi, kaynak kodda farklı noktalarda kullanılan her bir lambda ifadesi karşılığı derleyici tarafından oluşturulan sınıflar (closure types – kapanış sınıfları) birbirinden farklıdır. Yani her bir lambda ifadesi karşılığı derleyici eşsiz, tek bir sınıf oluşturulur:

Yukarıdaki kodda f1 ve f2 nesneleri aynı şekilde yazılan lambda ifadelerinden üretilmiş olsalar da farklı türlerdendir.
lambda ifadeleri için derleyicinin oluşturduğu kapanış sınıflarının (closure type) varsayılan kurucu işlevleri (default constructor) ve kopyalayan atama işlevleri (copy assignment operator function) delete edilmiştir.
(C++14 standartlarına göre bu işlevlerin delete edilmiş olma özellikleri kaldırılmıştır. C++14 standartlarında kapanış sınıflarının bu işlevleri yoktur.) Bu nedenle bir kapanış sınıfı türünden nesne varsayılan şekilde oluşturulamaz. Kapanış türlerinden nesneler birbirlerine atanamaz.

Oluşturulmuş bir lambda nesnesinin bir başka nesneye atanması ya da bir işlevden geri dönüş değeri olarak döndürülmesi için standart kütüphanenin function sınıf şablonu kullanılabilir. Aşağıdaki koda bakalım:

Yukarıdaki kodda foo işlevi kendi oluşturduğu kapanış nesnesini geri döndürüyor.

lambda ifadeleri ve STL

Yazımızın girişinde de belirttiğimiz gibi lambda ifadelerinin en sık kullanıldığı yer STL algoritmalarına yapılan çağrılardır. Aşağıdaki programa bakalım:

Kodda, main işlevi içinde tanımlanan name_list isimli list nesnesinin tuttuğu isimlerden len uzunluğunda olanlar silinmek isteniyor. Silme işlemi için remove-erase idiyomunun kullanıldığını görüyorsunuz. remove_if algoritmasının “predicate” bekleyen 3. parametresine bir lambda ifadesi gönderiliyor. lambda ifadesi kapsayan kod alanındaki len değişkenini yakalıyor.

lambda
ifadeleri şablonların tür parametreleri olarak da kullanılabilir:

Yukarıdaki main işlevinde oluşturulan setin ikinci şablon tür parametresi olarak bir lambda ifadesinden elde edilen kapanış türünün kullanıldığını görüyorsunuz.
C++14 standartları ile lambda ifadelerinin olanakları genişletildi. C++14 ve C++17 standartlarıyla gelen lambdalara ilişkin yeni araçları ve özellikleri bir başka yazımızda ele alacağız.

Share

Necati Ergin

C ve Sistem Programcıları Derneğinde eğitmen olarak çalışıyor.

Bunlar da ilginizi çekebilir

lambda ifadeleri (lambda expressions)” için 4 yorum

  1. Sayın Hocam aklınıza ve vucudunuza sağlık.
    Oldukça güzel anlatmışsınız.
    Biz bu konuları tarzanca seviyesinde bildiğimiz ingilizce ile anlamaya çalışıyoruz.
    Oysa bu konular bizim dilimizle bile anlaşılması zor oluyor.
    Bu da sizin, bize nasıl bir pencere açtığınızın göstergesidir.

    Teşekkürler.

  2. Hocam, ‘this adresinin yakalanması’ basliginda gecersiz ifade icin yazdiginiz gecerli ifade ayni olmus.
    Paylasimlarinizdan cok faydalaniyoruz. Tekrar tesekkurler…
    saygilarimla…

  3. Hocam lambda ifadelerle ilgili Türkçe’deki en geniş yazı olmuş. Teşekkür ederiz. Yeni yazılarınızın sabırla bekliyoruz.

Yorumlar kapatıldı.

Kod Eklemek İçin Okuyun