İçeriğe geç
academia.sh

Ders 10 / 11

Aralıklar

Bir sol değerden kurulan görünümün altındaki kapsayıcıyı aynen bıraktığının ve görünüm zincirinin tembel değerlendirildiğinin ölçümü.

Yayın Güncelleme

Bu derste

  • Bir görünümün altındaki kapsayıcıyı kopyalamadığını ve aynen bıraktığını ölçer
  • Görünüm zincirinin tembel değerlendirildiğini, sonuçların yalnız gezinme sırasında üretildiğini gösterir
  • views::filter ve views::transform'un birlikte nasıl bir işlem hattı kurduğunu açıklar
  • Sağ değerden kurulan bir görünümün kaynağı taşıyan yapıcıyla bir sahiplenen görünüme aldığını ve kaynağı boş bıraktığını ölçer
İçindekiler

Önceki ders bir lambdanın değerle yakaladığı bir değişkenin kopyasını kendi içine gömdüğünü ve dışarıdaki kaynağı aynen bıraktığını gösterdi. Bu ders C++20’nin aralık kütüphanesine (std::ranges) geçiyor ve benzer bir soru soruyor: bir görünüm (view), altındaki bir kapsayıcıdan kurulduğunda, o kapsayıcıyı kopyalıyor mu, yoksa yalnız ona bakan bir pencere mi oluyor?

Bir Görünüm Kapsayıcıyı Kopyalamaz

std::views::filter, verilen bir aralıktan yalnız bir koşulu sağlayan elemanları gösteren bir görünüm üretir. Bu görünüm kendi elemanlarını tutmaz; her gezinildiğinde altındaki aralığa bakar.

// gorunum-kopyalamaz.cpp — filter altindaki vector'u kopyaliyor mu
#include <cstdio>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> sayilar{1, 2, 3, 4, 5, 6};

    auto ciftler = sayilar | std::views::filter(
        [](int deger) { return deger % 2 == 0; });

    sayilar.push_back(8);

    std::printf("gorunumdeki elemanlar:");
    for (int deger : ciftler) {
        std::printf(" %d", deger);
    }
    std::printf("\n");
    std::printf("kaynak vector boyutu %zu\n", sayilar.size());
    return 0;
}
gorunumdeki elemanlar: 2 4 6 8
kaynak vector boyutu 7

KT33. ciftler görünümü oluşturulduktan sonra sayilar’a 8 eklendi ve görünüm gezinildiğinde bu yeni eleman da listeye girdi (8 çift olduğu için filtreyi geçti). Bu, ciftler’in sayilar’ın bir kopyasını almadığının kanıtı — kopya alsaydı, push_back’ten sonraki değişiklik görünümde görünmezdi. sayilar’ın kendisi bu görünüm oluşturma işleminden aynen çıktı: views::filter çağrısı kaynağa hiç yazmadı, yalnız ona bakan bir tanım kurdu. push_back ayrı bir işlemdi ve bu dersin ölçtüğü “görünüm kurma” adımı değil.

Görünüm Zinciri Tembel Değerlendirilir

Birden çok görünüm | işleciyle zincirlenebilir. std::views::transform, her elemana bir işlev uygulayan bir görünüm üretir. Bir zincir kurulduğunda, hiçbir eleman hemen işlenmez — işlem yalnız zincir gezinildiğinde, eleman eleman gerçekleşir.

// gorunum-zinciri-tembel.cpp — filter ve transform ne zaman calisiyor
#include <cstdio>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> sayilar{1, 2, 3, 4, 5, 6};

    auto zincir = sayilar
        | std::views::filter([](int deger) { return deger % 2 == 0; })
        | std::views::transform([](int deger) {
              std::printf("donusturuluyor: %d\n", deger);
              return deger * 10;
          });

    std::printf("zincir kuruldu\n");

    for (int deger : zincir) {
        std::printf("uretilen deger %d\n", deger);
    }
    return 0;
}
zincir kuruldu
donusturuluyor: 2
uretilen deger 20
donusturuluyor: 4
uretilen deger 40
donusturuluyor: 6
uretilen deger 60

KT34. “zincir kuruldu” satırı basıldığında transform’un içindeki printf hiç çalışmamıştı — filter ve transform çağrıları o ana kadar hiçbir elemanı işlemedi, yalnız bir tanım kurdular. “donusturuluyor” satırı ilk kez for döngüsü zinciri gezinmeye başladığında, tam da ilk elemanın sırası geldiğinde göründü, ve her “donusturuluyor” satırı kendi “uretilen deger” satırından hemen önce, teker teker çıktı — bir eleman işlenip yazdırılmadan bir sonrakine geçilmedi. Çıktının bu sırası (önce “zincir kuruldu”, sonra eleman eleman iç içe geçen “donusturuluyor”/“uretilen deger” çiftleri) zincirin ne zaman çalıştığını gösteriyor: kurulma anında değil, gezinme sırasında, eleman eleman. Kaynak sayilar bu zincirin hiçbir adımında değişmedi — aynen.

filter ve transform: Bir İşlem Hattı

İki görünümün zincirlenmesi, bir işlem hattı kurar: filter önce hangi elemanların geçeceğine karar verir, transform yalnız geçen elemanlara uygulanır. Bu sıra önemlidir — sıra değişse zincirin ürettiği sonuç da değişebilir.

// filter-transform-hatti.cpp — sira uretilen sonucu nasil etkiliyor
#include <cstdio>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> sayilar{1, 2, 3, 4, 5};

    auto once_filtrele_sonra_donustur = sayilar
        | std::views::filter([](int deger) { return deger % 2 == 0; })
        | std::views::transform([](int deger) { return deger * deger; });

    std::printf("once filtrele sonra donustur:");
    for (int deger : once_filtrele_sonra_donustur) {
        std::printf(" %d", deger);
    }
    std::printf("\n");
    return 0;
}
once filtrele sonra donustur: 4 16

KT35. Zincir önce çift sayıları (2, 4) seçti, sonra onları kareledi (4, 16). sayilar’ın kendisi bu işlem hattı boyunca hiç değişmedi — sayilar hâlâ {1, 2, 3, 4, 5}; aynen. once_filtrele_sonra_donustur adı, zincirin okunma sırasını (soldan sağa, filter önce) açıkça belirtiyor; bu, aralık zincirlerinin okunmasında kafa karışıklığını önleyen bir alışkanlık — zincir uzadıkça hangi adımın önce çalıştığını isimden değil zincirin kendi sırasından okumak gerekir.

Görünüm Kurmak 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. Görünümler bu soruyu hemen akla getirir: bir görünüm elemanları kendisi tutmaz, dolaşıldığında altındaki kapsayıcıdan okur. Ama bu ders “görünüm sonradan yapılan değişikliği görür mü” sorusunu ölçü olarak kullanmaz. Burada ölçülen, görünümü kurmanın kaynağa ne bıraktığıdır: sol değer bir kapsayıcıdan kurulan views::filter, views::transform ve views::take kaynağa yazmaz, kaynak aynen kalır. Sınırlayıcı ölçümde göreceğimiz gibi sağ değerden kurulan görünüm için yanıt başkadır.

views::take: Bir Aralığın İlk N Elemanı

std::views::take, verilen bir aralığın yalnız ilk n elemanını gösteren bir görünüm üretir. Diğer görünümler gibi bu da altındaki kapsayıcıyı kopyalamaz; yalnız gezinmeyi n eleman sonra durduran bir sınır kurar.

// views-take.cpp — take altindaki kapsayiciyi kopyaliyor mu
#include <cstdio>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> sayilar{10, 20, 30, 40, 50};

    auto ilk_uc = sayilar | std::views::take(3);

    std::printf("ilk uc:");
    for (int deger : ilk_uc) {
        std::printf(" %d", deger);
    }
    std::printf("\n");
    std::printf("kaynak vector boyutu %zu\n", sayilar.size());
    return 0;
}
ilk uc: 10 20 30
kaynak vector boyutu 5

KT36. ilk_uc görünümü yalnız ilk üç elemanı gösterdi, ama sayilar işlem sonunda hâlâ beş eleman taşıyor — boyutu değişmedi. views::take, views::filter’la aynı sınıfa giriyor: aynen. Fark, filter’ın hangi elemanların geçeceğine bir koşulla karar vermesi, take’in ise yalnız kaç elemanın geçeceğine bir sayıyla karar vermesi. İkisi de altındaki sayilar’a yazmadan, ona bakan farklı bir pencere kuruyor — bu, aralık görünümlerinin ortak özelliği: hepsi kaynağı aynen bırakır, yalnız o kaynağın hangi kısmının ya da hangi biçiminin görüneceğini değiştirir.

views::drop: Bir Aralığın Baştaki N Elemanını Atlamak

std::views::take’in tersi std::views::drop’tur: verilen aralığın ilk n elemanını atlayıp gerisini gösteren bir görünüm üretir. İkisi de altındaki kapsayıcıyı kopyalamaz, yalnız gezinmenin nereden başlayacağını ya da nerede duracağını belirleyen bir sınır kurar.

// views-drop.cpp — drop altindaki kapsayiciyi kopyaliyor mu
#include <cstdio>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> sayilar{10, 20, 30, 40, 50};

    auto son_iki = sayilar | std::views::drop(3);

    std::printf("son iki:");
    for (int deger : son_iki) {
        std::printf(" %d", deger);
    }
    std::printf("\n");
    std::printf("kaynak vector boyutu %zu\n", sayilar.size());
    return 0;
}
son iki: 40 50
kaynak vector boyutu 5

KT37. son_iki görünümü ilk üç elemanı (10, 20, 30) atladı ve kalan iki elemanı (40, 50) gösterdi; sayilar işlem sonunda hâlâ beş eleman taşıyor. views::take ve views::drop birbirini tamamlayan iki görünüm: biri “ilk n’i göster”, diğeri “ilk n’i atla” der, ikisi de kaynağı aynen bırakır. sayilar | std::views::take(3) ile sayilar | std::views::drop(3) art arda gezinilse, birbirini tamamlayan iki parça (10, 20, 30 ve 40, 50) elde edilir — aynı kaynağın iki farklı, ama birbiriyle örtüşmeyen görünümü.

std::ranges::sort: Yineleyici Çifti Yerine Doğrudan Aralık

Önceki derste görülen std::sort, bir yineleyici çifti (begin(), end()) ister. C++20’nin std::ranges::sort’u aynı işi doğrudan bir aralık nesnesiyle yapar — sayilar.begin()/sayilar.end() yazmaya gerek kalmaz. Sonuç aynı: kapsayıcı yerinde yeniden sıralanır.

// ranges-sort.cpp — ranges::sort yineleyici cifti istemiyor
#include <algorithm>
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> sayilar{5, 3, 1, 4, 2};

    std::ranges::sort(sayilar);

    std::printf("siralanmis:");
    for (int deger : sayilar) {
        std::printf(" %d", deger);
    }
    std::printf("\n");
    return 0;
}
siralanmis: 1 2 3 4 5

KT38. std::ranges::sort(sayilar) çağrısı, sayilar’ı yerinde yeniden sıraladı — bu işlemin sınıfı, önceki dersteki std::sort gibi değişti. Aradaki fark bir çağrı kolaylığı: std::ranges::sort bir aralık nesnesini doğrudan kabul eder, begin()/end() çağrılarını kendi içinde yapar. Bu, std::ranges ad alanındaki algoritmaların ortak bir özelliği — aynı iş, aralık nesneleriyle daha az yazımla ifade edilir; altta yatan karmaşıklık ve sonucun sınıfı (değişti/​aynen) değişmez.

Görünüm Kurmanın Karmaşıklığı: O(1), Gezinmenin Karmaşıklığı: Değişken

Bir görünüm zinciri kurmak (sayilar | std::views::filter(...) | std::views::transform(...)) sabit zamanlıdır — O(1). Bu satır çalıştığında hiçbir eleman ziyaret edilmez, hiçbir bellek ayrılmaz; kurulan şey yalnızca kaynağa bakan bir tanım, bir dizi lambda ve birkaç sınır değeridir. Asıl iş, zincir gezinildiğinde başlar ve orada karmaşıklık bağdaştırıcıya göre değişir.

views::transform ve views::filter ile bir sonraki elemana geçmek (operator++) her ikisinde de O(1) — ama filter’ın O(1)’i “amorti edilmiş” bir O(1)’dir: predikat elemanı reddederse, bir sonraki kabul edilen elemana ulaşana kadar iç döngü döner. En kötü durumda (hiçbir eleman predikatı geçmiyorsa) bir tek operator++ çağrısı O(n) sürebilir; ama zincir tümüyle gezinildiğinde toplam maliyet yine O(n)’dir, çünkü her eleman en fazla bir kez ziyaret edilir. views::take ve views::drop ise gerçek O(1): take, kaç eleman kaldığını sayar ve o sayıya ulaşınca durur; drop, rastgele erişimli bir aralık üzerinde (ör. vector) başlangıç noktasını doğrudan n adım ileri taşır, tek tek adımlamaz.

Bu ayrım, zincire hangi bağdaştırıcının eklendiğinin performans üzerinde görünmez bir etkisi olduğunu gösteriyor: kurma maliyeti her zaman O(1), ama gezinme maliyeti bağdaştırıcının türüne ve predikatın seçiciliğine bağlı. Bu dersin ölçümü — kaynağın aynen kaldığı — bu karmaşıklık farkından bağımsızdır; bir görünüm ister O(1) ister amorti edilmiş O(1) ile gezinsin, altındaki kapsayıcıyı hiçbir durumda değiştirmez. Karmaşıklık farkı yalnızca ne kadar sürdüğünü etkiler, kaynağın hangi sınıfa girdiğini değil.

Sınırlayıcı Ölçüm

Bu derste görünümlerin merkezi ölçümü aynen: sol değerden kurulan bir görünüm altındaki kapsayıcıyı hiçbir aşamada değiştirmez. Aynı kurma işlemini küçük bir değişiklikle — kaynağı bir sol değer yerine bir sağ değer olarak vererek — tekrarlamak sınıfı değiştirir. std::move(v) bir std::vector<int>&& üretir; bunu std::views::transform‘a yollamak, C++20’nin sahiplenen görünümünü (std::ranges::owning_view) devreye sokar: bağdaştırıcı, kendine verilen sağ değeri taşıyan yapıcıyla kendi içine alır. v’nin tuttuğu tampon (buffer) kopyalanmaz, sahiplenen görünümün kendi üyesine geçer; bu geçiş sabit zamanlıdır ve elemanlara tek tek dokunmaz. Sonuçta v taşınmış bir nesne olarak kalır — vector’ın taşıyan yapıcısı için bilinen davranış boş bir vector bırakmaktır, yani v.empty() artık evet verir. Kaynağın sınıfı boş’a düşer; oysa aynı zincir bir sol değerden kurulsaydı sınıf aynen kalırdı.

// sag-degerden-owning-view.cpp — sag deger vector owning_view icine tasinir
#include <cstdio>
#include <ranges>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};

    auto katlanmis = std::move(v) | std::views::transform([](int deger) { return deger; });

    std::printf("gorunum:");
    for (int deger : katlanmis) {
        std::printf(" %d", deger);
    }
    std::printf("\n");

    std::printf("v bos mu: %s\n", v.empty() ? "evet" : "hayir");
    return 0;
}
gorunum: 10 20 30
v bos mu: evet

Görünüm yine 10 20 30 veriyor, çünkü elemanlar hiçbir yere kopyalanmadı; yalnız kimin sahip olduğu değişti. v artık boş olsa da sahiplenen görünüm kendi içine taşıdığı tamponu tutmaya devam ediyor ve gezinme onun üzerinden çalışıyor. Bu boşalmanın dayanağı taşıyan yapıcıdır — taşıyan atama için aynı sonucu varsaymamak gerekir, çünkü atama zaten var olan bir hedefin üzerine yazar ve kaynağı boşaltıp boşaltmaması ayrı bir garantidir. Sahiplenen görünüm, adı üstünde, taşıdığı nesnenin ömrünü kendi ömrüne bağlar; bu yüzden yukarıdaki örnek güvenlidir ve kod olarak koşturulabilir. Buna karşılık, bir görünümün kapsayıcıya bakan bir referans tuttuğu ve o kapsayıcının ömrü görünümden önce bittiği bir durum tanımsız davranış üretir — böyle bir örnek burada yazılmaz ve koşturulmaz, çünkü tanımsız davranışın kendisi bu dersin konusu değil, yalnız var olduğunu bilmek önemlidir. C++20’nin owning_view’i tam olarak bu riski ortadan kaldırmak için tasarlandı: sağ değerden kurulan bir görünüm artık bir referans değil, kaynağın kendisini taşıyarak sahiplenir.

Özet

  • Bir görünüm (std::views::filter, std::views::transform) altındaki kapsayıcıyı kopyalamaz; kaynak kapsayıcı görünüm kurma işleminden aynen çıkar.
  • Görünüm zinciri tembel değerlendirilir: elemanlar zincir kurulduğunda değil, gezinildiğinde tek tek üretilir.
  • filter ve transform birlikte bir işlem hattı kurar; zincirdeki sıra hangi adımın önce uygulandığını belirler.
  • Bu dersin merkezi ölçümünün sınıfı aynen: bir görünüm, sol değerden kurulduğunda altındaki kapsayıcıyı hiçbir aşamada değiştirmez.
  • Sınırlayıcı ölçüm: aynı kurma işlemi sol değer yerine sağ değerle yapıldığında sınıf boş’a düşer — std::move(v) | std::views::transform(...) bir sahiplenen görünüm (owning_view) kurar, v’yi taşıyan yapıcıyla kendi içine alır ve v.empty() evet olur; görünüm yine 10 20 30 verir.

Sonraki Adım

Bu derste geçen auto zincir = sayilar | std::views::filter(...) | ... satırı, zincir’in gerçek tipini hiç yazmadı — o tip, filter ve transform’un iç içe geçmiş, adı olmayan bir görünüm tipi. Sonraki ders bu kursun kapanışı: auto ve decltype’ın bu tür tipleri nasıl çıkardığını ve tip çıkarımının kaynağa ne bıraktığını ele alı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