Ders 09 / 11
Lambdalar ve Fonksiyon Nesneleri
Bir lambdanın mutable olmayan değerle yakalamasının kaynağı aynen bıraktığının ve yakalanan kopyanın çağrılar arasında bağımsız kaldığının ölçümü.
Yayın Güncelleme
Bu derste
- Değerle yakalayan bir lambdanın dış değişkenin kendisini değiştirmediğini ölçer
- mutable anahtar sözcüğünün yakalanan kopyayı nasıl değiştirilebilir kıldığını gösterir
- Referansla yakalamanın değerle yakalamadan farkını ayırır
- Bir lambdanın derleyicinin ürettiği isimsiz bir işlev nesnesi olduğunu tanır
İçindekiler
Önceki ders std::remove’a bir değer geçirerek onu aradı; ama bazı
algoritmalar bir değer yerine bir yüklem ister — bir koşulu
değerlendiren, çağrılabilir bir şey. Bu ders o çağrılabilir şeyin en sık
kullanılan biçimine, lambda ifadelerine geçiyor. Bir lambda dış bir
değişkeni yakaladığında — köşeli parantezin içine [deger] yazdığında —
o dış değişkene ne oluyor?
Değerle Yakalama: Kopyalama, Bağlama Değil
[deger] yazan bir lambda, deger değişkeninin bir kopyasını lambda
nesnesinin içine gömer. Bu, dış deger’e bir referans tutmak değildir;
lambda oluşturulduğu anda değerin bir anlık görüntüsü alınır.
// deger-ile-yakalama.cpp — lambda disaridaki degiskeni degistiriyor mu
#include <cstdio>
int main() {
int deger = 10;
auto goster = [deger]() { std::printf("yakalanan deger %d\n", deger); };
deger = 99;
goster();
std::printf("disaridaki deger %d\n", deger);
return 0;
}
yakalanan deger 10 disaridaki deger 99
KT26. Lambda oluşturulduğunda deger 10’du ve lambda bu 10’un bir
kopyasını içine gömdü. Lambda çağrılmadan önce dışarıdaki deger 99’a
atandı, ama goster() çağrıldığında hâlâ 10 bastı — çünkü lambdanın
içindeki kopya, dışarıdaki değişkenden tamamen bağımsız. Dışarıdaki deger
kendisi de aynen kaldı: lambda oluşturma işleminin kendisi, yakaladığı
değişkeni hiç değiştirmedi; deger = 99 satırı zaten ayrı bir atama
işlemiydi ve bu dersin ölçtüğü “yakalama” işlemi değil.
mutable: Yakalanan Kopyayı Değiştirilebilir Kılmak
Varsayılan olarak değerle yakalanan bir kopya, lambdanın çağrı işlecinin
içinde const sayılır — değiştirilemez. mutable anahtar sözcüğü bu
kısıtlamayı kaldırır ve yakalanan kopyanın lambdanın gövdesi içinde
değiştirilmesine izin verir. Bu değişiklik yalnız kopyayı etkiler, dışarıdaki
değişkeni değil.
// mutable-yakalama.cpp — mutable ile kopyayi degistirmek
#include <cstdio>
int main() {
int adim = 0;
auto ilerle = [adim]() mutable {
adim += 1;
std::printf("lambda icindeki adim %d\n", adim);
};
ilerle();
std::printf("disaridaki adim %d\n", adim);
return 0;
}
lambda icindeki adim 1 disaridaki adim 0
KT27. mutable sayesinde adim’in lambda içindeki kopyası ilerle()
çağrısı sırasında 0’dan 1’e değişti — lambda nesnesi kendi kopyası
üzerinde bu değişikliği yapabildi. Ama dışarıdaki adim çağrıdan sonra da
0 olarak kaldı: aynen. Bu, mutable’ın neyi değiştirdiğini
netleştiriyor — dışarıdaki değişkeni değil, yalnız lambdanın kendi içine
gömdüğü kopyayı. Bu kursun merkezi ölçümü açısından bu dersin cevabı
burada: değerle yakalama, hangi mutable yazılırsa yazılsın, dışarıdaki
kaynağı hiçbir zaman değiştirmez.
Referansla Yakalama: Dış Değişkene Doğrudan Erişim
[°er] yazmak, kopya almak yerine dış değişkene bir referans yakalar.
Bu durumda lambdanın gövdesindeki değişiklik dışarıdaki değişkeni de
değiştirir — çünkü artık ayrı bir kopya yoktur.
// referans-ile-yakalama.cpp — referansla yakalama disariyi degistiriyor
#include <cstdio>
int main() {
int adim = 0;
auto ilerle = [&adim]() { adim += 1; };
ilerle();
std::printf("disaridaki adim %d\n", adim);
return 0;
}
disaridaki adim 1
KT28. Bu kez mutable yazmaya gerek kalmadı, çünkü değiştirilen şey bir
kopya değil, dışarıdaki adim’in kendisiydi. ilerle() çağrısından sonra
dışarıdaki adim 0’dan 1’e geçti: bu işlemin sınıfı değişti. Bu,
dersin başındaki değerle-yakalama örneğiyle doğrudan karşıt: [deger]
kaynağı aynen bırakırken [&adim] kaynağı değiştiriyor. Fark yakalama
listesindeki tek bir karakterde (&) ama sonucu tam tersi.
Toplu Yakalama: [=], [&] ve Karma Liste
Her dış değişkeni tek tek yazmak yerine [=] tüm dış değişkenleri değerle,
[&] tümünü referansla yakalar. İkisi karışık da yazılabilir: [=, °er]
diğer her şeyi değerle, yalnız deger’i referansla yakalar; [&, deger]
ise tam tersini yapar. Toplu yakalama, hangi değişkenin yakalanacağını
derleyiciye bırakır — lambda gövdesinde kullanılan her dış ad otomatik
olarak listeye eklenir.
// toplu-yakalama.cpp — karma yakalama listesi
#include <cstdio>
int main() {
int esik = 10;
int adim = 0;
auto degerlendir = [=, &adim](int deger) {
if (deger > esik) {
adim += 1;
}
};
degerlendir(15);
std::printf("disaridaki adim %d\n", adim);
std::printf("disaridaki esik %d\n", esik);
return 0;
}
disaridaki adim 1 disaridaki esik 10
KT29. [=, &adim] yakalama listesinde esik değerle (kopya olarak),
adim ise referansla yakalandı. degerlendir(15) çağrısı esik’in
kopyasını okudu ama değiştirmedi — dışarıdaki esik aynen kaldı.
Aynı çağrı adim’i referans üzerinden değiştirdi: dışarıdaki adim
0’dan 1’e geçti. Karma yakalama listesi, aynı lambda içinde bazı
değişkenlerin aynen kalmasını, bazılarının değişmesini aynı anda mümkün
kılar — sınıf kararı yakalama listesindeki her ad için ayrı ayrı verilir,
listenin tamamı için tek bir sınıf yoktur.
Lambda Aslında Bir İşlev Nesnesidir
Derleyici bir lambda ifadesi gördüğünde, arkada isimsiz bir sınıf üretir:
bu sınıfın bir operator() üye işlevi vardır ve yakalanan değişkenler bu
sınıfın veri üyeleridir. [deger] yakalaması, üretilen sınıfa deger
tipinde bir üye ekler ve o üyeyi yapıcıda kopyalar. Bu, lambdaların
elle yazılmış işlev nesneleriyle (bir operator() tanımlayan sıradan
sınıflar) neden aynı yerlerde kullanılabildiğini açıklar.
// islev-nesnesi-esdegeri.cpp — lambda ile elle yazilan sinif ayni isi yapiyor
#include <cstdio>
struct DegerGosterici {
int deger;
void operator()() const { std::printf("sinif icindeki deger %d\n", deger); }
};
int main() {
int deger = 42;
DegerGosterici gosterici_sinif{deger};
auto gosterici_lambda = [deger]() { std::printf("lambda icindeki deger %d\n", deger); };
gosterici_sinif();
gosterici_lambda();
return 0;
}
sinif icindeki deger 42 lambda icindeki deger 42
KT30. DegerGosterici sınıfı ile gosterici_lambda lambdası aynı
sonucu üretti, çünkü ikisi de aynı örüntüyü uyguluyor: bir değeri bir üye
olarak taşımak ve operator() içinde onu okumak. Bu çağrılardan hiçbiri
deger’i değiştirmedi — ikisi de aynen bırakıyor, çünkü ikisi de kendi
kopyalarını yapıcıda aldılar ve yalnız okudular. Bu denklik, lambdaların
“özel” bir dil özelliği değil, derleyicinin otomatik olarak yazdığı sıradan
bir sınıf olduğunu gösteriyor.
Yakalama Listesi Kaynağa Ne Bırakıyor
“C ile Sistem Programlama” kursu (c-sistem-programlama), bir değişikliğin
karşı tarafa ulaşıp ulaşmadığını sordu: yazanın kendi kopyası mı, aynı
nesneyi paylaşan taraflar mı, yoksa hiç kimse mi. Yakalama listesi o soruyu
hemen akla getirir — [°er] ile yakalayan lambda dışarıdaki değişikliği
“görür”, [deger] ile yakalayan görmez. Bu ders o soruyu sormuyor. Burada
ölçülen, lambda çağrıldıktan sonra kaynağın kendisinde ne kaldığıdır:
değerle yakalamada kaynak aynen, referansla yakalamada değişti, ilk
değerli yakalamayla taşındığında belirsiz. Görmek bir ilişkiyi, geride
kalan ise bir durumu adlandırır; bu kursun ölçüsü durumdur.
std::function: Farklı Çağrılabilirleri Aynı Tipte Tutmak
Her lambda, derleyicinin ürettiği kendine özgü, adı olmayan bir tipe
sahiptir — iki farklı lambda ifadesi, aynı imzayı taşısa bile farklı
tiplerdir. std::function<Sonuc(Argumanlar...)>, bu farklı tipleri tek bir
ortak kapta tutmayı sağlayan bir sarmalayıcıdır. Sarmalama işlemi, sarmalanan
çağrılabilirin kendisini değiştirmez; yalnız onun bir kopyasını taşır.
// std-function-sarmalama.cpp — std::function sarmaladigi lambdayi degistiriyor mu
#include <cstdio>
#include <functional>
int main() {
int carpan = 3;
auto lambda = [carpan](int deger) { return deger * carpan; };
std::function<int(int)> sarmali = lambda;
carpan = 100;
std::printf("sarmali(5) sonucu %d\n", sarmali(5));
std::printf("disaridaki carpan %d\n", carpan);
return 0;
}
sarmali(5) sonucu 15 disaridaki carpan 100
KT31. std::function<int(int)> sarmali = lambda; satırı, lambda’nın
o anki durumunun bir kopyasını sarmali’nin içine aldı — bu, tıpkı [carpan]
yakalamasının kendisi gibi bir kopyalama işlemi. carpan daha sonra 100’e
atansa da sarmali(5) hâlâ 15 verdi (5 * 3), çünkü sarmali’nin içindeki
kopya carpan’ın atama öncesindeki değerini taşıyordu. lambda’nın kendisi
bu sarmalama işleminden aynen çıktı: std::function’a atama, lambda
nesnesine hiç yazmadı, yalnız onu kopyaladı. Bu, dersin başındaki değerle
yakalama örneğiyle aynı örüntü — fark, bu kez kopyalanan şeyin bir int
değil, bir lambda nesnesinin kendisi olması.
Bu kopyalamanın bir bedeli vardır: std::function, farklı tiplerdeki
çağrılabilirleri tek bir ortak tipte tutabilmek için tip silme (type
erasure) uygular — sarmaladığı çağrılabilirin gerçek tipini bir taban sınıf
arkasına gizler. Bu gizleme genellikle bir dinamik bellek ayırma gerektirir
ve her çağrıda dolaylı bir zıplama (sanal çağrı benzeri bir mekanizma) ekler.
Doğrudan bir auto değişkenine atanan lambda ise kendi somut tipini korur;
derleyici çağrıyı doğrudan satır içine gömebilir. std::function bu yüzden
her yerde auto’nun yerini tutacak bir araç değildir — farklı tiplerdeki
çağrılabilirleri aynı kapta taşımak gerektiğinde (örneğin bir kapsayıcıda
callback listesi tutarken) tercih edilir, yalnızca tek bir lambdayı tutmak
için değil.
Genel Lambda: Parametre Tipi Olarak auto
C++14’ten itibaren bir lambdanın parametre listesinde somut bir tip yerine
auto yazılabilir. Bu, lambdanın çağrı işlecini bir şablona dönüştürür:
aynı lambda, farklı tiplerle çağrıldığında her tip için ayrı bir
operator() örneği üretilir. Bu, yakalama listesiyle ilgisizdir — yakalanan
değişkenlerin sınıfı (aynen/değişti) yine aynı kurallarla belirlenir; genel
olan yalnızca çağrı parametresidir.
// genel-lambda.cpp — auto parametreli lambda farkli tiplerle calisiyor
#include <cstdio>
int main() {
auto goster = [](auto deger) {
std::printf("deger olarak goster cagrildi\n");
(void)deger;
};
goster(7);
goster(3.14);
return 0;
}
deger olarak goster cagrildi deger olarak goster cagrildi
KT32. goster lambdası hem int hem double ile çağrıldı; derleyici
her çağrı için ayrı bir operator() örneği üretti, ama kaynak kodda tek bir
lambda ifadesi var. Bu, şablon işlevlerin genel işleyişiyle aynı mekanizma —
auto parametre, lambdanın çağrı işlecini bir şablon üye işlevine çevirir.
Bu dersin merkezi ekseniyle (yakalanan değişkenin sınıfı) doğrudan ilgisi
yok; genel lambda yalnızca çağrı parametresinin tipini esnekleştirir,
yakalama listesinin davranışını değiştirmez.
mutable Yazılmazsa
mutable olmayan bir lambdanın gövdesinde, değerle yakalanan bir
değişkene doğrudan atama yapmaya çalışan bir satır derlenmez — çünkü
operator(), mutable yazılmadığı sürece const olarak üretilir ve
const bir üye işlev içinde veri üyelerine atama yapılamaz.
// const-lambda-govdesi.cpp — mutable olmadan atama neden derlenmiyor
#include <cstdio>
int main() {
int deger = 0;
auto oku = [deger]() { std::printf("deger %d\n", deger); };
// Asagidaki satir derlenmez, cunku operator() const ve deger degistirilemez:
// auto bozuk = [deger]() { deger += 1; };
oku();
return 0;
}
deger 0
Bu kısıtlama bir eksiklik değil, güvencedir: mutable yazılmadığı sürece
bir lambdanın aynı yakalanan değeri her çağrıda aynı biçimde okuyacağı,
çağrılar arasında sessizce bir durum biriktirmeyeceği derleme anında
zorunlu kılınır. Bir lambdanın çağrılar arası durum tutması isteniyorsa bu
niyet mutable yazılarak açıkça belirtilmelidir.
Bu güvence, okuyan tarafın lambdayı incelerken hangi soruyu sorması
gerektiğini de belirler: yakalama listesinde & var mı yok mu, ve varsa
mutable yazılmış mı. Bu iki soruya verilen yanıt, o lambdanın yakaladığı
her değişken için kaynağın aynen mi yoksa değişti mi kalacağını
tek başına belirler; ayrıca bir çalışma zamanı gözlemine gerek kalmaz.
Sınırlayıcı Ölçüm
Üç lambda da aynı gövdeyi taşır — yakalanan adı bir kez artırıp yazdırır — ama yalnız yakalama biçimi değiştiğinde dışarıdaki kaynağın sınıfı üçe ayrılır:
#include <cstdio>
#include <string>
#include <utility>
int main() {
int adim = 0;
auto degerle = [adim]() mutable {
++adim;
std::printf("govde icinde: %d\n", adim);
};
degerle();
int konum = 0;
auto referansla = [&konum]() mutable {
++konum;
std::printf("govde icinde: %d\n", konum);
};
referansla();
std::string s = "veri";
auto tasiyarak = [t = std::move(s)]() {
std::printf("govde icinde: %s\n", t.c_str());
};
tasiyarak();
s = "yeni-deger";
std::printf("disaridaki adim: %d\n", adim);
std::printf("disaridaki konum: %d\n", konum);
std::printf("disaridaki s: %s\n", s.c_str());
return 0;
}
govde icinde: 1 govde icinde: 1 govde icinde: veri disaridaki adim: 0 disaridaki konum: 1 disaridaki s: yeni-deger
Üç yakalama da gövdenin içinde beklendiği gibi çalışır, ama dışarıdaki
kaynağa bıraktıkları iz üç farklı sınıfa düşer. [adim] değerle yakalar;
mutable kopyanın artırılmasına izin verse de bu artış yalnız lambdanın
kendi kopyasını etkiler, dışarıdaki adim aynen 0 kalır. [&konum]
referansla yakalar; gövdenin içindeki ++konum doğrudan dışarıdaki
değişkeni değiştirir, dışarıdaki konum değişti ve 1 oldu. [t = std::move(s)] ilk değerli bir yakalamadır: s’nin tuttuğu kaynak t’ye
taşınır ve taşıma sonrası s geçerli ama belirsiz bir durumda kalır —
bu yüzden bu ders s’nin taşıma sonrası içeriğini ya da uzunluğunu hiç
okumaz ve yazdırmaz. s’yi tekrar kullanılabilir kılan şey ona yeni bir
değer atamaktır: s = "yeni-deger" satırından sonra s artık tanımlı bir
içeriğe sahiptir ve onu okumak güvenlidir — çıktıdaki son satır bunu
gösterir. Aynı gövde, üç farklı yakalama biçimiyle kaynağı üç farklı
sınıfa bırakır: aynen, değişti, belirsiz.
Özet
[deger]ile değerle yakalama, dış değişkenin bir kopyasını alır; dış değişken lambda oluşturma işleminden aynen çıkar.mutable, yakalanan kopyayı lambda gövdesi içinde değiştirilebilir kılar, ama bu değişiklik yalnız kopyayı etkiler, dışarıdaki kaynağı değil.[°er]ile referansla yakalama, dış değişkenin kendisine erişir; bu durumda lambda gövdesindeki değişiklik dışarıyı da değiştirir.- Bir lambda, derleyicinin ürettiği isimsiz bir sınıfın örneğidir; yakalanan
değişkenler bu sınıfın veri üyeleri, lambdanın gövdesi ise
operator()’dür. - Bu dersin merkezi ölçümünün sınıfı aynen:
mutableolsun ya da olmasın, değerle yakalama dışarıdaki kaynağı hiçbir zaman değiştirmez. - Sınırlayıcı ölçüm: aynı gövde üç farklı yakalamayla üç farklı sınıf üretir — değerle yakalama aynen, referansla yakalama değişti, ilk değerli (taşıyan) yakalama kaynağı belirsiz bırakır ve bu ders o kaynağı yeniden atamadan önce hiç okumaz.
Sonraki Adım
Bu ders bir lambdanın yakaladığı değeri kendi içinde nasıl taşıdığını,
mutable’ın bu kopyayı ne zaman değiştirilebilir kıldığını ve referansla
yakalamanın neden dışarıdaki kaynağı doğrudan etkilediğini gösterdi.
Sonraki ders bir kapsayıcının elemanlarını dönüştürmeden, yeni bir
kapsayıcı oluşturmadan görüntülemenin yolunu ele alıyor: bir aralık görünümü
(view), altındaki kapsayıcıya bakan bir pencere olarak kaynağı nasıl aynen
bırakıyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.