Merhaba yazılımperver dostlarım, C++ 14 yazılarımıza devam ediyoruz.
Bu yazımda kalan C++ 14 özelliklerinden olan Jenerik Lambda (“Generik Lambda”) ve “lambda capture initializers” kabiliyetlerine bakıyor olacağız. Bu iki kabiliyeti, daha önce C++ 11 ile sunulan Lambda kabiliyetleri üzerene yapılan iyileştirmeler olarak görebilirsiniz. Bu sebeple eğer daha önce incelemediyseniz aşağıdaki yazıma göz atmanızı şiddetle tavsiye ediyorum:
Modern C++ (6) : Lambda İfadeleri
Ayrıca mevcut C++ 14 kabiliyetleri yolculuğundaki durumumuz için de C++ 14 Kabiliyetler Yolcuğu yazısından takip edebilirsiniz. Haydi başlayalım.
Jenerik Lambda İfadeleri (“Generic Lambda”)
Bu kabiliyeti kısaca özetlemek için aslında C++ 11 de sunulan ve C++ 14 ile eklenen hususlara, jenerik lambda perspektifinde bakmakta fayda var. C++ 11 ile lambdaya geçirilen fonksiyon parametreleri sadece “pass-by-value” ve “pass-by-reference” mekanizmaları ile geçirilebilmekteydi. Bir diğer ifade ile, taşıma semantiği ile sunulan tipleri lambdalar ile direk kullanmanız mümkün olamıyordu ve auto da kullanılamamaktaydı.
Bu arada, bu mekanizmalara ve taşıma semantiğine ilişkin pek bir fikriniz yok ise, bu yazıya devam etmeden önce bunlara ilişkin bir fikir sahibi olmanızda fayda var. Bu bağlamda, aşağıdaki yazım sizlere, hem bu mekanizmalar hem de jenerik lambdalar ile sunulan kabiliyeti anlamanıza yardımcı olabilir:
Modern C++ (5) : Taşıma Semantikleri
C++ 14 ile birlikte artık lambda fonksiyon parametreleri için de auto ve taşıma semantiğine uygun tipler kullanabiliyoruz. Bununla birlikte, herhangi bir ifade ile ilklendirme de mümkün olabilmekte (bir sonraki başlıkta bakıyor olacağız). Hemen basit bir örneğe bakalım:
1 2 3 4 5 6 7 8 |
void genericLambdaExample() { // Oncelikle bu C++ 11 ile mumkun degildi auto generic = [](auto x, auto y) { return x+y; }; // Int, int ve double, double için iki ayrı kullanım da artık mümkün std::cout << generic(1, 2) << std::endl; std::cout << generic(1.1, 2.2) << std::endl; } |
Burada gördüğünüz üzere, tek bir lambda ifadesini, farklı girdiler için kullanabiliyoruz ve derleyici sağolsun bizler için arka tarafta gerekli kodları üretiyor. Meraklı takipçilerimin bu kod parçasını hemen CompilerExplorer ile inceleyip, üzerinde oynayıp, arkada üretilen kodun nasıl değiştiğine baktığını umuyorum 🙂
Peki yukarıdaki kod parçasını derleyici nasıl değerlendiriyor ya da lambda’lar olmasaydı bunu nasıl elde ederdik? Aslında bakarsanız, temelde benzer davranışı aşağıdaki gibi bir kod parçası ile de elde edebiliriz:
1 2 3 4 |
struct _auto_generated_lambda_type { template <typename T, typename U> auto operator() (T x, U y) const {return x + y;} }; |
Tabi, bunun yanında artık “perfect forwarding” de mümkün olabilmekte, daha doğrusu taşıma mekanizmasını da kullanabileceğiz ve artık aşağıdaki kullanımlara mümkün olabilecek:
1 2 3 4 5 6 7 8 9 10 |
auto lamb1 = [](int&& x) {return x + 5; }; auto lamb2 = [](auto&& x) {return x + 5; }; int x = 10; // Burada açık olarak `std::move(x)` kullanmalıyız. Çünkü `int&&` bekliyoruz. lamb1(std::move(x)); // Bu da geçerli, çünkü burada x tipi `int&` olarak çıkarım yapabiliyor, çünkü auto kullanımı var. lamb2(x); |
Benzer şekilde, argümanların aktarılması ve “variadic” kullanım da mümkün:
1 2 3 4 5 |
// "Variadic" kullanıma örnek auto genericLambda = [](auto&&... args){return f(std::forward<decltype(args)>(args)...);}; // Argümanların aktarılmasına örnek auto genericLambda = [](auto&&... args){return f(decltype(args)(args)...);}; |
Yukarıdaki örnek kullanımlar yanında, jenerik lambdalar ile özyinelemeli lambda tanımlamaları da daha temiz ve okunabilir şekilde tanımlanabilmekteymiş. Örneğin aşağıda, fibonacci hesaplamak için kullanılabilecek bir kod parçasını görebilirsiniz:
1 2 3 4 5 6 7 |
auto fib = [](int n, auto&& fib) { if (n <= 1) return n; return fib(n - 1, fib) + fib(n - 2, fib); }; auto i = fib(7, fib); |
Bununla ne kadar işiniz olur bilemiyorum ama daha detaylı bilgi almak için https://artificial-mind.net/blog/2020/09/12/recursive-lambdas sayfasına göz atabilirsiniz.
Jenerik lambdaların son bir kullanımından daha bahsetmek istiyorum. O da, herhangi bir yakalama parametresi olmayan lambda ifadelerinin fonksiyon işsaretçilerine atanması olacak. Örneğin artık aşağıdaki ifade kullanılabilir olacak:
1 |
int (*fp)(int, char) = [](auto a, auto b){ return a + b; }; |
Sonuç olarak, bu kabiliyet ile ne elde ediliyor diye sorabiliriz? Şu elde ediliyor, modern C++ ile hedeflenen daha az kod satırı ile daha okunabilir ve idame edilebilir kodlar elde etmemize olanak sağlanıyor. Şu sayfadan bir örnek ile bu başlığı kapatalım:
C++11 ile yazılan aşağıdaki kodlar:
1 2 3 4 |
for_each( begin(v), end(v), [](decltype(*cbegin(v)) x) { cout << x; } ); sort( begin(w), end(w), [](const shared_ptr<some_type>& a, const shared_ptr<some_type>& b) { return *a<*b; } ); |
C++ 14 ile aşağıdaki hale dönüşebilmekteler:
1 2 3 |
for_each( begin(v), end(v), [](const auto& x) { cout << x; } ); sort( begin(w), end(w), [](const auto& a, const auto& b) { return *a<*b; } ); |
ikincisi daha okunabilir değil mi?
Lambda Capture Initializer
Bir önceki başlıkta ifade ettiğim, lambda ifade argümanları yanında, lambda kullanımlarında artık herhangi bir ifade de yakalanabilecek ve yakalama ifadesinde kullanılan değişkenin daha önce tanımlanmasına ihtiyaç kalmayacak. Örneğin aşağıdaki ifade C++ 14 öncesinde uyarı ya da hata verecektir, çünkü bir üst kapsamda value tanımlı değil.
1 2 3 4 |
int main() { auto lambda = [value = 1.5] { return value; }; return 0; } |
Jenerik lambdalar ile yukarıdaki kod parçası derlenebilir ve ilgili lambda ifadesi 1.5 döner ve bunun tipi de ilgili ifadeden çıkarılır. Aslında arkada yine auto kullanımını görüyoruz. Benzer şekilde taşıma mekanizması ile de yakalama yapılabilecek. Örneğin aşağıdaki kod içerisinde, nasıl taşıma yapıldığı görülebilir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <memory> #include <iostream> void bar(std::unique_ptr<int> p) { std::cout << "Deger: " << *p << "\n"; } int main() { std::unique_ptr<int> p(new int(1234)); auto f = [ptr = std::move(p)]() mutable { bar(std::move(ptr)); }; f(); return 0; } |
Ayrıca, yakalanan değişkenler, farklı isimlendirmelerle de kullanılabiliyor. Yine bir örnek ile bakalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> using namespace std; int main() { int x = 4; // x'i r ismi ile `pass-by-reference` mekanizmasi ile geciriyoruz // ayrica x olarak da yeniden isimlendiriyoruz auto lambda = [&rX = x, x = x + 1]()->int { rX += 2; // rX = 6 return x + 2; // 5 + 2, buradaki x, yeni isimlendirdigimiz }; // 7, 6 basilir cout << lambda() << ", " << x << endl; return 0; } |
Bu konulara ilave bilgilere erişmek isteyen takipçilerim için kaynaklar kısmına güzel bir iki site daha ekliyorum. Ayrıca, bu kabiliyetlere ilişkin standart öneri dokümanına da şu sayfadan ulaşabilirsiniz.
Bir sonraki yazımda görüşmek dileğiyle kendinize çok iyi bakın.
Kaynaklar
- https://isocpp.org/wiki/faq/cpp14-language#generic-lambdas
- https://artificial-mind.net/blog/2020/09/12/recursive-lambdas
- https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3559.pdf
- https://www.geeksforgeeks.org/generalized-lambda-expressions-c14/
- https://www.cppstories.com/2020/08/lambda-generic.html/