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.
filtervetransformbirlikte 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 vev.empty()evetolur; görünüm yine10 20 30verir.
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.