İçeriğe geç
academia.sh

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

[&deger] 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: [=, &deger] 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 — [&deger] 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.
  • [&deger] 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: mutable olsun 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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat