[:tr]Evet arkadaşlar, C++ yazılarımıza kaldığımız yerden devam ediyoruz.
Lambda yazım ile temel C++ 11 özelliklerini tamamlamış olacağız. Sonrasında belki STL için de bir yazı yazıp, daha sonra C++ 14/17/20’e yelken açabiliriz. Aslında oldukça geniş konu, fakat ben bu yazımda sizlere temel noktaları aktarmaya çalışacağım. Öncelikle benim gibi bilgisayar bilimleri ile uğraşanlarımız Lambda denilince hemen aklınıza Lambda calculus gelecektir ve bunlar arasında bir ilişki var mı diye de düşünebilirsiniz. Kısa cevap: bir Lisp kadar ilintili değil ama uzun cevap için aşağıdaki sonuçları için google amcamıza danışabiliriz. Bu arada Lisp “Lambda Calculus” ‘ın gerçek anlamda bir gerçeklemesi ve dilin bütün yapısı zaten jenerik programlamayı kolaylaştırmaya yönelik tasarlanmıştır.
Ayrıca “Lambda Calculus” ile ilgili de referanslar bölümüne bir kaç kaynak ekliyorum [6]. Bu konuda daha derinlemesine bilgi almak isteyen arkadaşlarım oraya yönelebilirler.
Lambda İfadeleri
Gelelim asıl mevzumuza Lambda İfadeleri. Hepimiz yazılım geliştirirken çeşitli işler için kısa kısa metotlar ve fonksiyonlar yazma ihtiyacı duymuşuzdur, hele de STL ile haşır neşirseniz, özellikle STL algoritma kütüphanesinde bu tarz kullanımlara çok sık ihtiyaç duyarsınız. Bunlar bir iki tane olunca sıkıntı olmasa da, sayıları arttıkça hem kod bu tarz satırlarla dolmakta, okunabilirlik azalmakta ve de bu kodların idamesi zorlaşmaktadır. İşte C++ 11 ile gelen Lambda ifadeleri ile artık:
– Kullanılacak olan fonksiyonlar ihtiyaç duyulan yerlerde tanımlayabilir,
– Yukarıdan aşağıya olan mantıksal ve doğal akışı koruyabilir (fonksiyonlar genelde farklı yerlerde tanımlanırlar, bu sebeple git geller okunabilirliği düşürebiliyor. Özellikle tek seferlik),
– Mevcut kapsam içerisindeki değişkenlere de erişim sunulabilmektedir.
Lambda ifadelerini tanımlayacak olursak. Lambda ifadeleri kısaca anonim fonksiyon olarak ifade edilebilir. Hatta bazı kaynaklar (cppreference) “Mevcut kapsamdaki değişkenleri
yakalayabilen isimsiz fonksiyon nesneleri olarak da tanımlar”. Bir diğer deyişle, lambda ifadeleri aslında çağrılabilecek olan bir kod birimini ifade etmektedirler. C++’dan önce aslında Haskell, C#, Java gibi bir çok dilde buna benzer, ihtiyaç duyulan yerde fonksiyon tanımlamasına olanak sağlayan yapılar sunulmaktaydı (tam ya da daha geniş bir liste için Wikipedia ya alalım sizi).
Meraklılar için lambdaların altında yatan mekanizmayı (derleyici lambda ifadelerini nasıl anlamlandırıyor veya arkada üretilen kod nedir, performansa bir etkisi var mı vs.) yazımın sonuna eklediğim ayrı bir bölümde anlatacağım.
Lambda ifadelerinin genel şablonu aşağıdaki gibi özetlenebilir. Burada * ile işaretlenen kısımlar opsiyonel olan parçalardır.
1 2 3 4 | [ Yakalama Tanımları ] *(Parametreler) *mutable -> dönüş tipi* { lambda işlevi tanımlaması } |
Detaylara inmeden önce aşağıda basit bazı lambda tanımlamaları ve bunların nasıl çağrıldığına ilişkin bir kaç örnek sizlere aktarayım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #include <iostream> using namespace std; int main() { // Parametre almayan ve dönüş yapmayan bir lambda ifadesi auto lambda = []() { cout << "Lambda ifadesi içeren kod!" << endl; }; lambda(); // Yukarıdaki ifade ile aşağıdaki ifadeler tamamen aynı işi yapıyorlar // auto lambda = [] { cout << Code within a lambda expression" << endl; }; // auto lambda = [](void) { cout << Code within a lambda expression" << endl; }; // auto lambda = [](void) -> void { cout << "Code within a lambda expression" << endl; }; // İki parametre alan bir lambda ifadesi örneği auto sum = [](int x, int y) { return x + y; }; cout << sum(5, 2) << endl; // Ayrıca lambda ifadelerini direk çağrıldığı yerde de tanımlayabilirsiniz cout << [](int x, int y) { return x + y; }(5, 2) << endl; } |
Yukarıda auto değişkenleri içerisinde lambda ifadelerine ilişkin işaretçiler tutulmakta. Bir de STL’e ilişkin örneğe bakalım hemen. Önce STL de mevcut durumda 5 ten büyük sayıları nasıl saydıracağımıza bakalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #include <iostream> #include <algorithm> #include <vector> using namespace std; // Bunun yerine fonksiyon nesnesi de oluşturabilirsiniz bool is_greater_than_5(int value) { return (value > 5); } int main() { vector<int> numbers { 1, 2, 3, 4, 5, 10, 15, 20, 25, 35, 45, 50 }; // Besten buyuk sayıları say auto greater_than_5_count = count_if(numbers.begin(), numbers.end(), is_greater_than_5); cout << "5 ten buyuk sayi adeti: " << greater_than_5_count << endl; } |
Şimdi de aynı işi lambdalar ile yapalım. Siz hangisi tercih edersiniz 🙂
1 2 3 4 5 6 7 8 9 10 11 12 | #include <iostream> #include <algorithm> #include <vector> using namespace std; int main() { vector<int> numbers { 1, 2, 3, 4, 5, 10, 15, 20, 25, 35, 45, 50 }; // Besten buyuk sayıları say cout << "5 ten buyuk sayi adeti: " << count_if(numbers.begin(), numbers.end(), [](int x) { return (x > 5); }) << endl; } |
Şimdi biraz daha derinlere inebiliriz. Önce lambdalar ile değişken yakalama mekanizmasına bakacağız daha sonra da parametre geçirme ve dönüş mekanizmalarını inceleyeceğiz. Bu arada lambda ifadelerinin arka planı için ise son bölüme başvurabilirsiniz.
Lambda ifadeleri ile değişken yakalama (“Variable Capture Mechanism”):
Normal fonksiyonlardan farklı olarak lambda ifadeleri, mevcut tanımlandığı blokta/kapsamda (“scope”) tanımlanmış olan değişkenlere de erişim sağlayabilmektedir. Buna “Variable Capture Mechanism” diğer bir ifade ile Değişkenleri Yakalama Mekanizması diyebiliriz.
Bu mekanizma ile aşağıdaki şekillerde değişkenleri yakalayabiliriz:
– Değer/Kopya yakalama (“Capture by value”)
– Referans yakalama (“Capture by reference”)
– Hem değer hem de referans yakalama (“Capture by both value and reference (mixed capture) “)
Detaylarını örnekler ile aşağıda anlatacağım ama özetleme adına bu kullanımlara ilişkin yazım şekillerinin özetini aşağıda bulabilirsiniz:
- [&] : tanımlandığı kapsam içerisindeki bütün değişkenlerin referansları yakalanır/kullanılabilir (capture all external variables by reference)
- [=] : tanımlandığı kapsam içerisindeki bütün değişkenlerin kopyaları yakalanır/kullanılabilr (capture all external variables by value)
- [a, &b] : a değişkeninin kopyasını, b değişkeninin de referansını yakalar/kullanır
- [] : herhangi bir ortam değişkeni yakalanmaz ve sadece lambda ifadeleri içerisindeki yerel değişkenlere erişiminiz olur
Şimdi bu zamana kadar yazdıklarımıza ilişkin yakama örnekleri üzerinden kullanımlarına bakalım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | int x=1, y=1, z=1; // 1. Ornek [ ] ( ) { cout << “Example 1”; } ( ); // 2. Ornek [ ] ( ) { cout << x; } (); // 3. Ornek [x] ( ) { cout << x+1; } (); // 4. Ornek [=] ( ) { cout << x << y; } (); // 5. Ornek auto tmp = [ ] ( ) { cout << "merhaba"; } (); |
Yukarıdaki 1. örnekte herhangi bir değişken yakalanmadığı duruma örnek (kullanım 4). 2. örnek ise geçerli bir tanımlama değil, çünkü x değişkeni lambda ifadesine herhangi bir şekilde geçirilmemiş. 3. örnek bir önceki örneğin geçerli hali ve sadece kopyasının geçirildiği duruma örnek teşkil etmektedir. 4. örnek de benzer şekilde z dışındaki bütün değişkenler lambda ifadesi içerisine kopyalanarak geçirilmekte. z kullanılmadığı için geçirilmemektedir. Son örnek te geçerli bir satır değil. Neden sizce? Biraz düşünün sebebi yazının sonunda (ipucu diğer satırlardan farkı nedir “()” ne yapar).
Şimdi biraz da değişkenlerin referans olarak yakalanmasına ilişkin bir kaç örnek inceleyelim.
1 2 3 4 5 6 7 8 9 10 | int x=1, y=1, z=1; // Bütün değişkenler referans olarak geçirilmekte [&] ( ) { cout << x << y << z; }( ); // z referans olarak geçirilmekte, diğerlerinin kopyası geçirilmekte [=,&z] ( ) { z = x + y; }( ); // x 'in kopyası geçirilmekte, diğerleri referans olarak geçirilmekte [&,x] ( ) { y = x; z = x; }( ); |
Bu kullanımların yanında lambda ifadelerini sınıf metotları içerisinde de kullanabilirsiniz. Fakat burada bir hususa dikkat etmeniz gerekiyor. Lambdaların her bir için derleyici özel ve ayrı bir sınıf oluşturmakta ve bu sebeple çalışma zamanında hepsinin kendi kapsamları oluyor. Bir diğer ifade ile sınıf metotları içerisindeki lambda ifadeleri içerisinde bulundukları sınıf değişkenlerine ulaşamazlar. Bunun için ilgili sınıfın işaretçisi “this” ile geçirilmesi gerekmektedir. Hemen bir örnek ile inceleyelim:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Filtreleyici { public: Filtreleyici(vector<int>& src) : mDataToFilter(src) { } void Filter() { remove_if(mDataToFilter.begin(), mDataToFilter.end(), [this](int i){ return ( i < mFilterReference); }); // Asagidaki kullanim gecerli degil // [](int i){ return ( i < mFilterReference); }); private: vector<int> mDataToFilter; int mFilterReference; }; |
Parametre Geçirme ve Dönüş Değerleri:
Kapsam yakalama ve değişkenlerin lambda ifadelerine geçirilmelerinden sonra lambda ifadelerine parametrelerin geçirilmesi hususuna eğilelim. Daha önce de bahsettiğim gibi lambda ifadelerine normal fonksiyonlar gibi parametreler geçirebiliyoruz. Lambda ifadeleri için de normal fonksiyonlara uygulanan parametre geçirme kuralları uygulanmakta. C++ 14 e kadar normal fonksiyonlardan farklı olarak varsayılan parametre değerlerini lambdalarda kullanamıyoruz, C++ 14 de bu kısıtlama da ortadan kalkmakta. Lambda ifadelerindeki parantezlerde aslında isteğe bağlı eğer herhangi bir parametre geçirilmiyor ise “[] {}” tamamen geçerli bir kullanım (hatta en basit kullanım diyebiliriz).
Lambda ifadelerinde normal fonksiyonlarda olduğu gibi değerler dönülebilmektedir. Fonksiyonlardan farklı olarak, lambdalarda dönüş değerleri direk ifade edilebilmeleri yanında derleyici tarafından otomatik olarak da belirlenebiliyor. Eğer lambda tanımlaması içerisinde birden fazla dönüş ifadesi var ise ya da dönüş tipi çıkarımında bulunamayacağı durumlarda dönüş tipi de lambda ifadesinde belirtilmelidir. Ayrıca yazılan kodta bir dönüş işlemi “return deyimi” yok ise lambda işlevin geri dönüş değerinin türü void kabul edilir.
Not olarak lambda ifadelerinde throw kullanımı da mümkün (normal C++ standardında throw kullanımı “depreceated” olarak işaretlense de dilden tamamen henüz çıkarılmadı).
Şimdi bu son hususlara ilişkin örnek kodları inceleyelim:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // İki tane kopyalama yöntemi kullanılarak parametreler geçiriliyor // Bu durum için dönüş değeri belirtmeye gerek yok (int olarak derleyici kendisi bu çıkarımda bulunabilir) [ ] (int x, int y) {return x + y; } (1,2); // Hatalı ifade // İlk parametre referans olarak geçirileceği ifade edilmiş ve bu rvalue parametreye sabit bir değer geçirilmiş [ ] (int &x, int y) {return x + y; } (1,2); // Lambda ifadesi ilk parametreyi sabit referans olarak alıyor ikincinin ise kopyasını alıyor // Bu durumda da dönüş tipi belirtilmeli [ ] (const int &x, int y) -> int {int z = 3; return x + y + z; } (1,2); // Yerel de tanımlanmış bir değişken referans olarak dönülmüş (tabi bu doğru ve tercih edilen bir kullanım değil ;) [ ] (int x, int y) -> int& {int z = 0; return z; } (1,2); Birazda dönüş değerlerine ilişkin örnekleri inceleyelim: // Açıkça dönüş değeri belirtildiği durum int p1 = [ ] (int x, int y) -> int {return x+y;} (1,2); // Dönüş değeri belirtilmeyen durumlar. Burada derleyici tipi belirliyor (int) int p2 = [ ] (int x, int y) -> {return x+y;}(1,2); // Derleme hatası. Belirtilen ve dönülen tipler arasında uyumsuzluk var int p4 = [ ] () -> int {return false;}(); // Derleme hatası. Belirtilen kodtan dönüş değerinin belirlenmesi mümkün değil. // Açıkça tipinin ifade edilmesi gerekiyor int p5 = [ ] (int x) { if x > 5 return x; else return true;}(1); |
STL Kullanım:
Yukarıda aslında STL’e kullanıma ilişkin bir kaç örnek vermeye çalıştım. Aşağıda çok bilindik sıralama fonksiyonu için olan kullanıma bakalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // Klasik yaklaşım class comparator { public: bool operator() (int x, int y) const { return x > y; } }; sort (v.begin(), v.end(), comparator()); // Lambda kullanımı ile gelen yeni yaklaşım sort(v.begin(), v.end(), [ ] (int x, int y) { return x > y;}); |
Bu kullanım yanında lambda ifadeleri şablonlara (“template”) parametre olarak ta geçirilebilirler. Aşağıda buna ilişkin bir örnek kullanım görebilirsiniz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #include <set> #include <functional> #include <iostream> int main() { // recommended std::set< int, std::function< bool(int,int) > > exampleSetWithLambda([] ( int x, int y ) { return x > y ; } ) ; exampleSetWithLambda.insert(2); exampleSetWithLambda.insert(1); exampleSetWithLambda.insert(5); std::cout << "Sirali:" << std::endl; for(auto item : exampleSetWithLambda) { std:: cout << item << " "; } std::cout<< std::endl; std::set< int > exampleSetWithNoLambda; exampleSetWithNoLambda.insert(2); exampleSetWithNoLambda.insert(1); exampleSetWithNoLambda.insert(5); std::cout<< "Sirasiz:" << std::endl; for(auto item : exampleSetWithNoLambda) { std:: cout << item << " "; } } |
Bunların yanında std::function kullanarak lambda lari metotlara da geçirebilirsiniz.
C++ Lambda larının Çalışma Mekanizması
Lambda’lar yazımın başında da bahsettiğim üzere aslında çağrılabilir küçük kod parçaları olarak adlandırabiliyor. Diğer fonksiyonlardan farklı olarak içerisinde bulunduğu kapsamda tanımlı olan değişkenlere erişim de sağlayabilmekte. Peki mevcut mekanizmalardan farkı nedir? Yani normal fonksiyon, functor sınıflarından ( operator() tanımlayan sınıflar) ne farkı var? Bu başlıkta buna bakacağız.
Basit olarak aslında siz bir lambda tanımlaması yaptığınız zaman, derleyici arka planda sizler için bir “functor” sınıfı oluşturuyor. Bu sınıfların her biri eşsiz ve her tanımlama için ayrı ayrı oluşturuluyor. Örneğin:
1 | [](X& elem) { elem.op(); } |
için derleyici:
1 2 3 4 5 6 7 8 | class _DerleyiciTarafindanAtanmisIsim_ { public: void operator()(X& elem) const { elem.op(); } }; |
tanımı oluşturuyor. Bu bir anlamda “syntatic sugar” dediğimiz yani işimiz kolaylaştıran bir mekanizma olarak düşünebiliriz. Şimdi biraz daha derine inelim ve assembly kodlarını karşılaştıralım :).
Önce hiç bir değişkenin yakalanmadığı duruma bakalım. Bu arada bu kodlar (x86-64 gcc 8.2 derleyicisinden çıkan kodlar).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | int function (int a) { return a + 3; } class Functor { public: int operator()(int a) { return a + 3; } }; int main() { auto lambda = [] (int a) { return a + 3; }; Functor functor; volatile int y1 = function(5); volatile int y2 = functor(5); volatile int y3 = lambda(5); return 0; } |
Normal fonksiyon tanımı için aşağıdaki gibi bir kod üretilmekte:
1 2 3 4 5 6 7 8 | traditionalFunction(int): push rbp mov rbp, rsp mov DWORD PTR [rbp-4], edi mov eax, DWORD PTR [rbp-4] add eax, 3 pop rbp ret |
“Functor” için ise aşağıdaki gibi kod üretilmekte:
1 2 3 4 5 6 7 8 9 | Functor::operator()(int): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi mov eax, DWORD PTR [rbp-12] add eax, 3 pop rbp ret |
Lambda için ise aşağıdaki gibi:
1 2 3 4 5 6 7 8 9 | main::{lambda(int)#1}::operator()(int) const: push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi mov eax, DWORD PTR [rbp-12] add eax, 3 pop rbp ret |
herhangi bir değişken yakalanmadığı durumda görüleceği üzere “functor” ile lambda ifadelerine ilişkin üretilen kod aynı. Standart metot ile ise ufak bir fark var.
Şimdi ortam değişkenlerini kopyalayarak yakalama durumunu inceleyelim. Bu durumda normal metotları kullanamayacağız. Sadece “functor” lar ile lambdaları karşılaştıracağız.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | class Functor { public: Functor(const int x) : m_x(x) { } int operator()(int a) { return a + m_x; } private: int m_x; }; int main() { int x = 3; auto lambda = [=] (int a) { return a + x; }; Functor functor(x); volatile int y1 = functor(5); volatile int y2 = lambda(5); return 0; } |
Burada fonksiyon nesneleri için bizi ilgilendiren iki metot var. Bunlar yapıcı ve () operatörü. Şimdi bunlar için
üretilen kodlara bakalım:
1 2 3 4 5 6 7 8 9 10 11 | Functor::Functor(int): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi mov rax, QWORD PTR [rbp-8] mov edx, DWORD PTR [rbp-12] mov DWORD PTR [rax], edx nop pop rbp ret |
Yapıcıya ilişkin kod kısaca aslında esi yazmacı içeriğini rdi yazmacı ile ifade edilen belleğe kopyalamaya karşılık geliyor.
Buna geçirilen değerleri görmek için de main() içerisinde buna ilişkin üretilen koda bakalım.
1 2 3 4 5 6 7 8 9 | // int x = 3; mov DWORD PTR [rbp-4], 3 // Functor functor(x); mov edx,DWORD PTR [rbp-0x4] lea rax,[rbp-0x20] mov esi,edx mov rdi,rax call Functor::Functor(int) |
Buradan x’in rbp-0x4 yazmacında saklandığını ve daha sonra dolaylı yoldan esi yazmacına yazıldığını görüyoruz. rbp-0x20 adresini rdi atıldığını görüyoruz. lea komutu ve sonrasındaki satırlar ile bizim fonksiyon nesnemizi saklayan adres.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Functor::operator()(int): // int operator()(int a) push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi // return a + m_x; mov rax, QWORD PTR [rbp-8] mov edx, DWORD PTR [rax] mov eax, DWORD PTR [rbp-12] add eax, edx pop rbp ret |
Asıl () operatörü için üretilen kod ile yapıcı için üretilen kod birbirine oldukça benzemekte. En büyük fark ise m_x in değerini okumak için çağrılan iki satır komut.
Şimdi lambda ifadesi için üretilen koda bakalım:
1 2 3 4 5 6 7 8 9 10 11 | main::{lambda(int)#1}::operator()(int) const: push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi mov rax, QWORD PTR [rbp-8] mov edx, DWORD PTR [rax] mov eax, DWORD PTR [rbp-12] add eax, edx pop rbp ret |
Bu kod parçasının da fonksiyon nesne objesi için üretilen ile aynı olduğu görülebilir. Burada tabi lambda ifadesinin oluşturulmasına ilişkin kodunun nerede olduğu sorulabilir.
Onun için main içerisine bakıyoruz:
1 2 | mov eax, DWORD PTR [rbp-4] mov DWORD PTR [rbp-16], eax |
Görüleceği üzere fonksiyon nesne objesinin yapıcısı için üretilen kod, lambda için üretilen kodtan oldukça fazla. Bunun da sebebi yapıcı için olan oluşturucu kodu normal üretilen kodun içerisine gömülmekte. Ortam değişkenlerinin referans olarak geçirildiği durumda aslında yukarıdakine benzer tek fark değer yerine işaretçilerin geçirilmesi.
Bu kullanımlara baktığımızda özellikle fonksiyon nesneleri ile lambda ifadeleri birbirlerinin hemen hemen aynısı. Ana farklılıklar:
– “functor” lar ve lambda ifadeleri fazladan bir this göstergeci geçiriyorlar (fazladan 8 byte),
– Lambda ifadelerine ilişkin yapıcı kodları lambda ifadelerinin içerisine yediriliyor ve bu sayede kopyalamaya ilişkin fazladan oluşturulan kod miktarı azaltılmış oluyor.
Sonuç olarak lambda ifadelerinin performans anlamında bir yük getirmediğini ifade edebiliriz ([8] de daha detaylı bir performans karşılaştırması görebilirsiniz).
Yine kısa dedik sözü uzattık 🙂 olsun lambda ifadeleri oldukça önemli ve nispeten yeni bir kabiliyet, siz yazılımperverlere faydası olduysa ne ala 🙂
Kaynaklar:
- https://www.cprogramming.com/c++11/c++11-lambda-closures.html
- https://www.geeksforgeeks.org/lambda-expression-in-c/
- https://web.mst.edu/~nmjxv3/articles/lambdas.html
- https://blog.feabhas.com/2014/03/demystifying-c-lambdas/
- https://www.wikiwand.com/en/Lambda_calculus
- https://www.inf.fu-berlin.de/lehre/WS03/alpi/lambda.pdf
- https://en.cppreference.com/w/cpp/language/lambda
- https://vittorioromeo.info/index/blog/passing_functions_to_functions.html
[:en]Yes folks, we are continuing to our C++ posts where we left.
We will complete the core C++ 11 features with this Lambda Expression post. Then I may write a post about STL features that come with C++ 11, after that we will explore the C++ 14/17/20.
In fact, lambda expression is very wide topic in my opinion, but I will try to focus on most important aspects of lambda expression in a brief manner (well at the end of post, brief word may lost is meaning). If you are also a computer scientist as me, when I say lambda, the first thing that will bear in your mind would be the lambda calculus. You may also start to wonder if lambda calculus and lambda expressions that come with c++ 11 are same or somehow related. The short answer is: not as much as Lisp, but for more detailed answer I will kindly request you to consult google. By the way, the Lisp is one real implementation of lambda calculus and all language structure is designed specifically to make generic programming easy.
I am also adding a reference [6] for Lambda Calculus for curious Yazilimperver fellows 😉
Lambda Expressions
Now let us start to talk about Lambda Expressions. As you know and experience, most of us needed to write short methods and functions during programming. If you are using STL library (I hope you are using it), especially, the algorithm library, you will require such short methods, functors more often. When there is one or two short method, this may not be an issue, but as the number of methods increases, the code bloated with this kind of short methods, eventually its readibility, maintainibaility decreases. With C++ 11 lambda expressions, now you can:
– Define anonymous methos where they required,
– The natural execution flow from up to down is preserved (in case of standalone functions or functors which are usually defined in different places, you may need to jump to there and jump back),
– You may access the variables defined in given scope.
Let us now define the lambda expressions. Briefly, they can be defined as anonymous function. Some references (cppreference) define them as “An unnamed function object capable of capturing variables in scope”. In other words, lambda expressions define anonymous callable function definitions. Before C++ 11, other programming languages also provide lambda expression as their language feature such as C#, Java, Haskell (for a more complete list of programming languages and corresponding structs you can consult to sir Wikipedia).
Enthusiastic readers that are curious about underlying lambda expressions mechanism (how compiler interpret them and the code generated for lambda expressions as well as perfomance considerations), I prepared a separate section for you at the end of my post, check it out.
General syntax of lambda expressions can be outlined as follow. Here the items that are marked as “*” are optional.
1 2 3 4 | [ Capture Statements ] *(Parameters) *mutable -> Return Type* { Lambda epxression statement } |
Before delving into details, let us explore some simple lambda expressions and their usage through simple example codes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #include <iostream> using namespace std; int main() { // A lambda expression with no return type and parameters auto lambda = []() { cout << "A code with lambda expression!" << endl; }; lambda(); // The following statements are completely same with above expression // auto lambda = [] { cout << Code within a lambda expression" << endl; }; // auto lambda = [](void) { cout << Code within a lambda expression" << endl; }; // auto lambda = [](void) -> void { cout << "Code within a lambda expression" << endl; }; // A lambda expression that takes two parameters auto sum = [](int x, int y) { return x + y; }; cout << sum(5, 2) << endl; // We can also call the lambda expression where they defined cout << [](int x, int y) { return x + y; }(5, 2) << endl; } |
The above auto variables hold pointers to defined lambda expressions. Now let us look at simple STL usage example where code will count the numbers that are greater than five:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | #include <iostream> #include <algorithm> #include <vector> using namespace std; // We can also define a functor for this method bool is_greater_than_5(int value) { return (value > 5); } int main() { vector<int> numbers { 1, 2, 3, 4, 5, 10, 15, 20, 25, 35, 45, 50 }; // Count the numbers greater than 5 auto greater_than_5_count = count_if(numbers.begin(), numbers.end(), is_greater_than_5); cout << "The count of numbers greater than 5: " << greater_than_5_count << endl; } |
Now let us to the same thing with lambda expression and see which one do you prefer 🙂
1 2 3 4 5 6 7 8 9 10 11 12 | #include <iostream> #include <algorithm> #include <vector> using namespace std; int main() { vector<int> numbers { 1, 2, 3, 4, 5, 10, 15, 20, 25, 35, 45, 50 }; // Count the numbers greater than 5 cout << "The count of numbers greater than 5: " << count_if(numbers.begin(), numbers.end(), [](int x) { return (x > 5); }) << endl; } |
Now we can dive into more details. First of all, we will look at the variable capture mechanism, then we will explore the parameter passing and return mechanisms. By the way, as I said before, you can check the last section for underlying of lambda expressions.
Variable Capturing with Lambda Expressions (“Variable Capture Mechanism”):
Different from traditional functions, lambda expressions can also capture/access to variables that are defined in their scope. This is also called as “Variable Capture Mechanism“.
Using this mechanism, the variables can be captured as follow (the names should be familiar to one that knows parameter passing mechanisms):
– Capture by value
– Capture by reference
– Mixed capture, capture by both value and reference
These usage of these mechanisms can be summarized as follow. We will later look into some examples about these:
- [&] : all variables in scope where lambda expression defined can be captured/used by reference (capture all external variables by reference)
- [=] : all variables in scope where lambda expression defined can be captured/used by value (capture all external variables by value)
- [a, &b] : copy of a and reference of b are captured
- [] : no variables can be accessed only the local variables defined in lambda expression statement can be used.
Now let us look at some variable capture mechanism examples:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | int x=1, y=1, z=1; // Example 1 [ ] ( ) { cout << “Example 1”; } ( ); // Example 2 [ ] ( ) { cout << x; } (); // Example 3 [x] ( ) { cout << x+1; } (); // Example 4 [=] ( ) { cout << x << y; } (); // Example 5 auto tmp = [ ] ( ) { cout << "Hello"; } (); |
Example 1 is an example of no parameter passing (Capture mechanism 4). Example 2 is not a valid definition, because x variable is not passed/captured through parameters or capture list. Example 3 is valid version of previous example and problem is resolved by passing mentioned x variable through capture list. In example 4, all variables except z is passed to lambda definition. z is not passed because it is not used in lambda expression. Last example is also not valid. Why do you think? I will give the answer at the end of post (clue: what does () at the end does).
Now let us see some examples for capturing variables by reference.
1 2 3 4 5 6 7 8 9 10 | int x=1, y=1, z=1; // All variables are captured by reference [&] ( ) { cout << x << y << z; }( ); // z is captured by reference, other variables are passed by value [=,&z] ( ) { z = x + y; }( ); // x is captured by value, others are captured by reference [&,x] ( ) { y = x; z = x; }( ); |
In addition to these usage examples, the lambda expressions can also be used in class method definitions. There is one important point that should be careful about. For each lambda expression, a separate and unique class is created and as a result each of lambda expression have their own scopes. In other words, lambda expressions that are defined in class members can not access the class data members that are defined. To do so, “this” keyword is used inside capture brackets. Let us see an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class Filtreleyici { public: Filtreleyici(vector<int>& src) : mDataToFilter(src) { } void Filter() { remove_if(mDataToFilter.begin(), mDataToFilter.end(), [this](int i){ return ( i < mFilterReference); }); // Asagidaki kullanim gecerli degil // [](int i){ return ( i < mFilterReference); }); private: vector<int> mDataToFilter; int mFilterReference; }; |
Passing Parameters and Return Types:
In this section, we will look at parameter passing and returning value mechanisms. As I said above, parameters can be passed to lambda expression like traditional functions. The rules for function parameter passing is also same for lambda expression parameter passing. Till C++ 14 you can not use default values with lambda parameters but with C++ 14, this is also possible. The paranthesises “()” in lambda expressions are also opitonal and []{} is a valid lambda expression (simplest lambda expression).
As in traditional functions, lambda expression can also return values. Different from functions, compiler can deduce the return data type automatically for you, but of course you can explicitly define them. If there are more than one return types then you should explicitly define return type. Besides, if there is no return keyword in lambda body statement then its return type is marked as void.
As an additional note, throw can also be used by lambda expressions, but is marked as depreceated so might be removed in future standards.
Let us now examine example codes for this topic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // İki tane kopyalama yöntemi kullanılarak parametreler geçiriliyor // Bu durum için dönüş değeri belirtmeye gerek yok (int olarak derleyici kendisi bu çıkarımda bulunabilir) [ ] (int x, int y) {return x + y; } (1,2); // Hatalı ifade // İlk parametre referans olarak geçirileceği ifade edilmiş ve bu rvalue parametreye sabit bir değer geçirilmiş [ ] (int &x, int y) {return x + y; } (1,2); // Lambda ifadesi ilk parametreyi sabit referans olarak alıyor ikincinin ise kopyasını alıyor // Bu durumda da dönüş tipi belirtilmeli [ ] (const int &x, int y) -> int {int z = 3; return x + y + z; } (1,2); // Yerel de tanımlanmış bir değişken referans olarak dönülmüş (tabi bu doğru ve tercih edilen bir kullanım değil ;) [ ] (int x, int y) -> int& {int z = 0; return z; } (1,2); Birazda dönüş değerlerine ilişkin örnekleri inceleyelim: // Açıkça dönüş değeri belirtildiği durum int p1 = [ ] (int x, int y) -> int {return x+y;} (1,2); // Dönüş değeri belirtilmeyen durumlar. Burada derleyici tipi belirliyor (int) int p2 = [ ] (int x, int y) -> {return x+y;}(1,2); // Derleme hatası. Belirtilen ve dönülen tipler arasında uyumsuzluk var int p4 = [ ] () -> int {return false;}(); // Derleme hatası. Belirtilen kodtan dönüş değerinin belirlenmesi mümkün değil. // Açıkça tipinin ifade edilmesi gerekiyor int p5 = [ ] (int x) { if x > 5 return x; else return true;}(1); |
STL Usage:
In above lines, I tried to give you some examples about STL usage. Below you can find an example lambda expression usage in STL for sorting:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // Classic approach class comparator { public: bool operator() (int x, int y) const { return x > y; } }; sort (v.begin(), v.end(), comparator()); // New Lambda expression approach. Much cleaner! sort(v.begin(), v.end(), [ ] (int x, int y) { return x > y;}); |
In addition to above usage, lambda expressions can be used as template parameter. An example is provided below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #include <set> #include <functional> #include <iostream> int main() { // recommended std::set< int, std::function< bool(int,int) > > exampleSetWithLambda([] ( int x, int y ) { return x > y ; } ) ; exampleSetWithLambda.insert(2); exampleSetWithLambda.insert(1); exampleSetWithLambda.insert(5); std::cout << "Ordered:" << std::endl; for(auto item : exampleSetWithLambda) { std:: cout << item << " "; } std::cout<< std::endl; std::set< int > exampleSetWithNoLambda; exampleSetWithNoLambda.insert(2); exampleSetWithNoLambda.insert(1); exampleSetWithNoLambda.insert(5); std::cout<< "Unordered:" << std::endl; for(auto item : exampleSetWithNoLambda) { std:: cout << item << " "; } } |
You can also pass lambda expressions to functions through std::function.
C++ Lambda Expression Underhood
As you may understand, lambda expression are nothing but simple executable code fragments. Different from traditional functions, they can capture/access/cahnge the variables in their scope (Variable capture mechanism).
So what is the difference? In other words, what make lambda expression different than tradition functions, functors? In this section, we will investigate this question.
In its simplest form, when you define a lambda expression, compiler generate a functor class for you at backstage. All these classes are unique and separately generated. For instance:
1 | [](X& elem) { elem.op(); } |
For above line, compiler generate following class:
1 2 3 4 5 6 7 8 | class _CompilerAssignedName_ { public: void operator()(X& elem) const { elem.op(); } }; |
From this perspective, lambda expressions can be considered as syntatic sugar for functors. Now let us dive into more deeper parts and compare the generated assembly codes :).
Let us first look at the case where no variable is capture. By the way, for assembly x86-64 gcc 8.2 compiler is employed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | int function (int a) { return a + 3; } class Functor { public: int operator()(int a) { return a + 3; } }; int main() { auto lambda = [] (int a) { return a + 3; }; Functor functor; volatile int y1 = function(5); volatile int y2 = functor(5); volatile int y3 = lambda(5); return 0; } |
The generated code for tradition function definition is provided below:
1 2 3 4 5 6 7 8 | traditionalFunction(int): push rbp mov rbp, rsp mov DWORD PTR [rbp-4], edi mov eax, DWORD PTR [rbp-4] add eax, 3 pop rbp ret |
The generated code for functor definition is provided below:
1 2 3 4 5 6 7 8 9 | Functor::operator()(int): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi mov eax, DWORD PTR [rbp-12] add eax, 3 pop rbp ret |
And finally, the generated code for lambda expression is provided below:
1 2 3 4 5 6 7 8 9 | main::{lambda(int)#1}::operator()(int) const: push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi mov eax, DWORD PTR [rbp-12] add eax, 3 pop rbp ret |
In case of no variable capturing, the code generated for functors and lambda expressions are completely same. There are some minor difference amont traditional function call.
Now let us explore the case where variables are captured by value. In this case, no tradition method is used as it is not possible for them to access the variables defined in given scope. Only functors will be employed for comparison.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | class Functor { public: Functor(const int x) : m_x(x) { } int operator()(int a) { return a + m_x; } private: int m_x; }; int main() { int x = 3; auto lambda = [=] (int a) { return a + x; }; Functor functor(x); volatile int y1 = functor(5); volatile int y2 = lambda(5); return 0; } |
Here, two methods are important for us which are constructor and () operator. Let us explore generated code for these two methods:
1 2 3 4 5 6 7 8 9 10 11 | Functor::Functor(int): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi mov rax, QWORD PTR [rbp-8] mov edx, DWORD PTR [rbp-12] mov DWORD PTR [rax], edx nop pop rbp ret |
Briefly, constructor code copies the content of esi register to the memory represented with rdi register.
To see the variables passed, let us look at the code generated inside main() function
1 2 3 4 5 6 7 8 9 | // int x = 3; mov DWORD PTR [rbp-4], 3 // Functor functor(x); mov edx,DWORD PTR [rbp-0x4] lea rax,[rbp-0x20] mov esi,edx mov rdi,rax call Functor::Functor(int) |
This code tells us x is stored at rbp-0x4 register and finally stored at esi register and rbp-0x20 address is stored at rdi register. The lines that start with lea command is about storing our function objects.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Functor::operator()(int): // int operator()(int a) push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi // return a + m_x; mov rax, QWORD PTR [rbp-8] mov edx, DWORD PTR [rax] mov eax, DWORD PTR [rbp-12] add eax, edx pop rbp ret |
The code that is generated for constructor and () operator is very similar. The most obvious difference is additional two lines that are used for reading m_x.
Now let us look at code generated for lambda expression:
1 2 3 4 5 6 7 8 9 10 11 | main::{lambda(int)#1}::operator()(int) const: push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov DWORD PTR [rbp-12], esi mov rax, QWORD PTR [rbp-8] mov edx, DWORD PTR [rax] mov eax, DWORD PTR [rbp-12] add eax, edx pop rbp ret |
As you can see, this generated code is same as the one generated for functors. Here, you may ask about the code used for creating lambda expression. We are looking into main() for that portion:
1 2 | mov eax, DWORD PTR [rbp-4] mov DWORD PTR [rbp-16], eax |
As it can be seen from the code portions generated, the code that is generated for functor creation is much more than one that is generated for lambda expression. Main reason for this is embedding lambda construction code directly into lambda expression code. In case of capturing by reference, instead of passing variables, pointers are captured.
After having look at the code generated, it can be seen that generated code for lambda expression and functor is very similar. The most important differences are:
– functors and lambda expressions are passing an extra this pointer which is not the case for traditional functions (additional 8 byte)
– Lambda construction code is embedded into code generated for lambda expression statements which prevent extra copy cost and amount of code generated.
In conclusion, we can say that lambda expressions does not impose additional performance overhead. (A more detailed performance analysis can be found in [8]).
References:
- https://www.cprogramming.com/c++11/c++11-lambda-closures.html
- https://www.geeksforgeeks.org/lambda-expression-in-c/
- https://web.mst.edu/~nmjxv3/articles/lambdas.html
- https://blog.feabhas.com/2014/03/demystifying-c-lambdas/
- https://www.wikiwand.com/en/Lambda_calculus
- https://www.inf.fu-berlin.de/lehre/WS03/alpi/lambda.pdf
- https://en.cppreference.com/w/cpp/language/lambda
- https://vittorioromeo.info/index/blog/passing_functions_to_functions.html
[:]

Faydalı bir yazı olmuş
Teşekkürler
Geri bildiriminiz için çok teşekkürler.
teşekkür ederim benim için çok faydalı oldu. Cümleleriniz anlatım tarzını çok güzel.
Türkçe kaynak oluşturduğunuz ve -blokta/kapsamda (“scope”)- bu tarz bilgisayar terimlerini türkçeleştirerek kullandığınız içinde ayrıca teşekkür ederim. Kaliteli içerikleri okumanın keyfi bir başka oluyor.
emeğinize sağlık 🙂
Faydalı oldu ise ne mutlu. Güzel geri bildiriminiz için ben teşekkür ederim.