[:tr]Modern C++ (6) : Lambda İfadeleri[:en]Modern C++ (6) : Lambda Expressions[:]

[: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.

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.

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:

Şimdi de aynı işi lambdalar ile yapalım. Siz hangisi tercih edersiniz 🙂

Ş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:

  1. [&] : tanımlandığı kapsam içerisindeki bütün değişkenlerin referansları yakalanır/kullanılabilir (capture all external variables by reference)
  2. [=] : tanımlandığı kapsam içerisindeki bütün değişkenlerin kopyaları yakalanır/kullanılabilr (capture all external variables by value)
  3. [a, &b] : a değişkeninin kopyasını, b değişkeninin de referansını yakalar/kullanır
  4. [] : 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.

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.

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:

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:

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:

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:

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:

için derleyici:

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).

Normal fonksiyon tanımı için aşağıdaki gibi bir kod üretilmekte:

“Functor” için ise aşağıdaki gibi kod üretilmekte:

Lambda için ise aşağıdaki gibi:

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.

Burada fonksiyon nesneleri için bizi ilgilendiren iki metot var. Bunlar yapıcı ve () operatörü. Şimdi bunlar için
üretilen kodlara bakalım:

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.

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.

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:

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:

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:

  1. https://www.cprogramming.com/c++11/c++11-lambda-closures.html
  2. https://www.geeksforgeeks.org/lambda-expression-in-c/
  3. https://web.mst.edu/~nmjxv3/articles/lambdas.html
  4. https://blog.feabhas.com/2014/03/demystifying-c-lambdas/
  5. https://www.wikiwand.com/en/Lambda_calculus
  6. https://www.inf.fu-berlin.de/lehre/WS03/alpi/lambda.pdf
  7. https://en.cppreference.com/w/cpp/language/lambda
  8. 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.

Before delving into details, let us explore some simple lambda expressions and their usage through simple example codes.

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:

Now let us to the same thing with lambda expression and see which one do you prefer 🙂

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:

  1. [&] : all variables in scope where lambda expression defined can be captured/used by reference (capture all external variables by reference)
  2. [=] : all variables in scope where lambda expression defined can be captured/used by value (capture all external variables by value)
  3. [a, &b] : copy of a and reference of b are captured
  4. [] : 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:

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.

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:

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:

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:

In addition to above usage, lambda expressions can be used as template parameter. An example is provided below:

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:

For above line, compiler generate following class:

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.

The generated code for tradition function definition is provided below:

The generated code for functor definition is provided below:

And finally, the generated code for lambda expression is provided below:

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.

Here, two methods are important for us which are constructor and () operator. Let us explore generated code for these two methods:

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

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.

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:

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:

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:

  1. https://www.cprogramming.com/c++11/c++11-lambda-closures.html
  2. https://www.geeksforgeeks.org/lambda-expression-in-c/
  3. https://web.mst.edu/~nmjxv3/articles/lambdas.html
  4. https://blog.feabhas.com/2014/03/demystifying-c-lambdas/
  5. https://www.wikiwand.com/en/Lambda_calculus
  6. https://www.inf.fu-berlin.de/lehre/WS03/alpi/lambda.pdf
  7. https://en.cppreference.com/w/cpp/language/lambda
  8. https://vittorioromeo.info/index/blog/passing_functions_to_functions.html

[:]

4 Comments [:tr]Modern C++ (6) : Lambda İfadeleri[:en]Modern C++ (6) : Lambda Expressions[:]

  1. elif

    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 🙂

    1. yazılımperver

      Faydalı oldu ise ne mutlu. Güzel geri bildiriminiz için ben teşekkür ederim.

Comments are closed.