İçeriğe geç
academia.sh

Ders 06 / 11

Yineleyiciler ve Geçerlilik

erase sonrası bir yineleyicinin okunmasının tanımsız davranışa girdiğinin ve geçerli kalan yineleyicilerin aynen kaldığının ölçümü.

Yayın Güncelleme

Bu derste

  • erase edilen bir elemana ait yineleyicinin neden bir daha okunamadığını gösterir
  • Hangi kapsayıcı işlemlerinin yineleyicileri geçersizleştirdiğini, hangilerinin geçersizleştirmediğini ayırır
  • Geçersizleşmeyen bir yineleyicinin işlemden sonra aynen kaldığını ölçer
  • Yineleyici kategorileri arasındaki (girdi, ileri, çift yönlü, rastgele erişim) farkı tanır
İçindekiler

Önceki ders stok["armut"] çağrısının haritaya sıfırla kurulmuş bir eleman eklediğini, bunun kaynağı değiştirdiğini ölçtü. Orada değişen şey kapsayıcının bütünüydü: eleman sayısı ve haritanın içeriği. Bu ders sınırı daha küçük bir birime indiriyor: kapsayıcının içindeki tek bir konuma işaret eden bir yineleyici. Bir eleman silindiğinde, o elemana işaret eden yineleyici elde tutulmaya devam edilirse ne olur? Bu sorunun cevabı kapsayıcıdan kapsayıcıya değişir — aynı erase çağrısı bir vector’da bütün sonraki yineleyicileri etkilerken bir list’te yalnız silinen elemanın kendisini etkiler — bu yüzden “yineleyici hâlâ geçerli mi” sorusu yalnız işlemin adına bakılarak değil, hangi kapsayıcı üzerinde çağrıldığına bakılarak yanıtlanabilir.

erase Sonrası Bir Yineleyiciyi Okumak

std::vector::erase, verilen konumdaki elemanı kaldırır ve ondan sonraki elemanları bir konum kaydırır. Kaldırılan elemana ait yineleyici bu işlemden sonra geçersizdir — artık var olmayan bir belleğe işaret eder ya da üzerine başka bir elemanın taşındığı bir konuma işaret eder. Bu yineleyiciyi okumak tanımsız davranıştır ve bu ders onu hiç çalıştırmaz; yalnız erase’in döndürdüğü yeni, geçerli yineleyiciyi kullanır.

// erase-donen-yineleyici.cpp — erase hangi yineleyiciyi geçerli birakiyor
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> sayilar{10, 20, 30, 40};
    auto it = sayilar.begin() + 1; // 20'yi gosteriyor

    auto sonraki = sayilar.erase(it);

    std::printf("erase sonrasi eleman sayisi %zu\n", sayilar.size());
    std::printf("erase'in dondurdugu konumdaki deger %d\n", *sonraki);
    return 0;
}
erase sonrasi eleman sayisi 3
erase'in dondurdugu konumdaki deger 30

KT7. erase(it) çağrısından sonra it değişkeni artık geçerli değil — bu ders it‘i erase’den sonra bir daha hiç okumuyor, çünkü okumak tanımsız davranışa girer ve tanımsız davranış bu kursta hiç çalıştırılmaz. Onun yerine erase’in döndürdüğü sonraki yineleyicisi kullanılıyor: standart, erase’in kaldırılan elemandan sonraki konuma işaret eden yeni ve geçerli bir yineleyici döndüreceğini garanti eder. Bu işlemin sınıfı belirsiz: it’in kendisinin işlemden sonraki durumu standart tarafından tanımlanmamıştır, tek güvence onu okumamak gerektiğidir.

Geçersizleştirmeyen İşlemler: Aynen Kalan Yineleyiciler

Her değiştirici işlem yineleyicileri geçersizleştirmez. std::list::erase, yalnız silinen elemana ait yineleyiciyi geçersizleştirir — diğer tüm yineleyiciler geçerliliğini korur, çünkü bağlı bir liste elemanları bitişik bellekte tutmaz ve bir düğümün silinmesi diğer düğümleri taşımaz.

// list-erase-diger-yineleyiciler.cpp — silinmeyen elemanin yineleyicisi ne oluyor
#include <cstdio>
#include <list>

int main() {
    std::list<int> degerler{10, 20, 30};
    auto orta = std::next(degerler.begin());       // 20
    auto son = std::next(degerler.begin(), 2);      // 30

    degerler.erase(orta);

    std::printf("silinmeyen yineleyicinin degeri %d\n", *son);
    std::printf("kalan eleman sayisi %zu\n", degerler.size());
    return 0;
}
silinmeyen yineleyicinin degeri 30
kalan eleman sayisi 2

KT8. son yineleyicisi, orta silinirken hiç dokunulmayan bir düğüme işaret ediyordu ve erase(orta) çağrısından sonra da aynı düğüme işaret etmeye devam ediyor — değeri hâlâ 30. Bu yineleyicinin durumu aynen: silinen eleman kendisi olmadığı için standart onun geçerliliğini garanti eder. std::vector için bu garanti yoktur — bir vector’de bir eleman silindiğinde ondan sonraki tüm elemanlar bir konum kaydığından, silinen konumdan sonraki her yineleyici de geçersizleşir. Bu fark, hangi kapsayıcının seçildiğinin yineleyici kullanımını doğrudan etkilediğini gösterir.

Yineleyici Kategorileri

Standart kütüphane yineleyicileri işlevsel kapasitelerine göre kategorilere ayırır. Girdi yineleyicisi yalnız ileri doğru, tek geçişlik okuma yapabilir. İleri yineleyici aynı yineleyiciyle birden çok geçiş yapmayı destekler. Çift yönlü yineleyici (std::list’in yineleyicisi gibi) hem ileri hem geri gidebilir ama sabit zamanlı atlama yapamaz. Rastgele erişim yineleyicisi (std::vector’ın yineleyicisi gibi) it + n gibi sabit zamanlı atlamayı destekler.

// yineleyici-kategorisi.cpp — rastgele erisim ve cift yonlu farki
#include <cstdio>
#include <iterator>
#include <list>
#include <vector>

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

    auto vit = v.begin() + 3; // sabit zamanli atlama
    auto lit = std::next(l.begin(), 3); // dogrusal zamanda ilerler

    std::printf("vector uzerinden atlanan deger %d\n", *vit);
    std::printf("list uzerinden ilerlenen deger %d\n", *lit);
    return 0;
}
vector uzerinden atlanan deger 4
list uzerinden ilerlenen deger 4

KT9. İki yineleyici de aynı sonuca ulaştı ama farklı karmaşıklıkla: v.begin() + 3 vector’ın rastgele erişim yineleyicisi olduğu için sabit zamanda hesaplanır; std::next(l.begin(), 3) ise list’in çift yönlü yineleyicisi üzerinde üç kez ++ çağrısına eşdeğerdir ve doğrusal zamanda ilerler. Bu ders hız farkını ölçmüyor — süre bu kursta hiç ölçülmüyor — yalnız iki kategorinin farklı işlemleri desteklediğini gösteriyor: list yineleyicisi üzerinde it + 3 yazmak derlenmez, çünkü çift yönlü yineleyici operator+ sağlamaz.

Aralık Tabanlı for ve Örtük Yineleyiciler

Aralık tabanlı for döngüsü, arka planda begin() ve end() çağırarak yineleyicileri örtük kullanır. Döngü gövdesinde kapsayıcıya eleman eklenirse ya da elemanlar silinirse, örtük yineleyiciler de aynı geçerlilik kurallarına tabidir — bu yüzden bir vector üzerinde aralık tabanlı for gövdesinde push_back çağırmak, döngünün geri kalanını tanımsız davranışa sokabilir. Bu ders böyle bir değişikliği hiç yapmıyor.

// araligbdan-for-salt-okuma.cpp — araligb tabanlı for elemanlari nasil gezer
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> sayilar{5, 10, 15};
    int toplam = 0;

    for (int deger : sayilar) {
        toplam += deger;
    }

    std::printf("toplam %d\n", toplam);
    std::printf("eleman sayisi degisti mi %s\n",
        (sayilar.size() == 3) ? "hayir" : "evet");
    return 0;
}
toplam 30
eleman sayisi degisti mi hayir

KT10. Döngü yalnız her elemanı okudu, kapsayıcıya hiç yazmadı; sayilar işlem sonunda aynen kaldı. Örtük begin()/end() çağrıları da bu yüzden döngü boyunca geçerli kaldı — kapsayıcının boyutu ya da içeriği değişmediği için yineleyicilerin geçersizleşeceği bir durum hiç oluşmadı.

Boş ile Belirsiz Arasındaki Fark

end() yineleyicisi ile geçersizleşmiş bir yineleyici bazen aynı şeymiş gibi karıştırılıyor; ikisi de “okunmaması gereken” bir konumu gösteriyor gibi görünüyor. Ama standart bu ikisine tamamen farklı garantiler bağlıyor. end(), kapsayıcının son elemanından bir sonraki konumu gösteren, her zaman tanımlı ve karşılaştırmaya açık bir değerdir — it == kapsayici.end() yazmak her koşulda geçerlidir, çünkü end()’in kendisi hiçbir zaman geçersiz değildir; yalnız *end() ile onu dereferanslamak tanımsızdır. Bir kapsayıcı boş olduğunda begin() == end() olur — bu, “aranan eleman yok” demenin tanımlı bir yoludur, find’ın bulamama durumunda döndürdüğü değer de budur. Geçersizleşmiş bir yineleyici ise belirsizdir: değeri karşılaştırmaya bile açık değildir, it == baska_it yazmak da tanımsız davranışa girebilir, çünkü standart geçersizleşmiş bir yineleyicinin hangi işlemleri desteklediğini hiç garanti etmez. Bu fark önemlidir çünkü bir kodun “boş” durumunu denetlemesi (it == end()) her zaman güvenlidir, ama “acaba bu yineleyici hâlâ geçerli mi” sorusunu yineleyicinin kendisine sorarak yanıtlamak mümkün değildir — geçerlilik yineleyicide taşınan bir bilgi değildir, kaynağın geçirdiği işlemlerden çıkarılması gereken bir bilgidir.

push_back ve Yeniden Ayırma: Bütün Yineleyicileri Etkileyen İşlem

erase yalnız silinen konumdan sonrasını etkiler; std::vector::push_back ise çok daha geniş bir hasar bırakabilir. vector’ın ayrılmış kapasitesi dolmuşsa push_back yeni, daha büyük bir bellek bloğu ayırır, tüm mevcut elemanları oraya taşır ve eski bloğu serbest bırakır — bu durumda eklemeden önce alınmış her yineleyici, işaretçi ve referans geçersizleşir, yalnız eklenen elemana ait olanlar değil. Kapasite yeterliyse taşıma hiç olmaz ve mevcut yineleyiciler aynen kalır; ama çağıran taraf bunu size() ve capacity()’yi karşılaştırmadan önceden bilemez.

// push-back-yeniden-ayirma.cpp — kapasite dolunca push_back yineleyicileri nasil etkiler
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> sayilar;
    sayilar.reserve(2);
    sayilar.push_back(1);
    sayilar.push_back(2);

    bool kapasite_doldu = (sayilar.size() == sayilar.capacity());
    sayilar.push_back(3);

    std::printf("ekleme oncesi kapasite dolmus muydu %s\n",
        kapasite_doldu ? "evet" : "hayir");
    std::printf("eleman sayisi %zu\n", sayilar.size());
    std::printf("ilk eleman %d\n", sayilar.front());
    return 0;
}
ekleme oncesi kapasite dolmus muydu evet
eleman sayisi 3
ilk eleman 1

KT11. reserve(2) ile ayrılan kapasite iki eleman eklendiğinde tam doldu; üçüncü push_back çağrısı bu yüzden vector’ı yeni bir bloğa taşıdı. Taşıma tamamlandıktan sonra kapasiteden önce alınmış olabilecek her yineleyici belirsiz duruma girerdi — bu ders böyle bir yineleyiciyi saklayıp taşımadan sonra okumuyor, yalnız front() ile kapsayıcının kendisinden yeniden okuyor. front()’un 1 döndürmesi, elemanların değerinin taşımadan etkilenmediğini gösteriyor; etkilenen şey elemanların değeri değil, onlara önceden alınmış yineleyicilerin geçerliliği.

std::map’te Yineleyici Kararlılığı

İlişkisel kapsayıcılar bu konuda vector’dan çok list’e benzer. std::map bir dengeli ikili arama ağacıyla gerçeklenir ve elemanlar bitişik bellekte durmaz; bir düğüm eklendiğinde ya da silindiğinde diğer düğümler yer değiştirmez, yalnız ağacın bağlantıları güncellenir. Bu yüzden std::map::erase de tıpkı std::list::erase gibi yalnız silinen elemana ait yineleyiciyi geçersizleştirir; kalan her yineleyici, işaretçi ve referans aynen geçerli kalır. std::map::insert da benzer bir garanti taşır: yeni bir düğüm eklemek var olan düğümleri hiç taşımaz, dolayısıyla eklemeden önce alınmış hiçbir yineleyici geçersizleşmez.

// map-erase-diger-yineleyiciler.cpp — map erase diger yineleyicileri etkiler mi
#include <cstdio>
#include <map>

int main() {
    std::map<int, int> tablo{{1, 10}, {2, 20}, {3, 30}};
    auto uc = tablo.find(3);

    tablo.erase(tablo.find(1));

    std::printf("silinmeyen anahtarin degeri %d\n", uc->second);
    std::printf("kalan eleman sayisi %zu\n", tablo.size());
    return 0;
}
silinmeyen anahtarin degeri 30
kalan eleman sayisi 2

KT12. tablo.find(1)’e ait yineleyici silindi, ama uc başka bir anahtara işaret ediyordu ve erase çağrısından etkilenmedi — uc->second hâlâ 30. Bu durumun sınıfı std::list’teki karşılığıyla aynı: aynen. std::vector’da aynı senaryo kurulsaydı — üç elemandan ilkini silmek — kalan elemanlar kaydığı için ikinci ve üçüncü elemana ait yineleyiciler geçersizleşirdi; burada geçersizleşmiyor olması map’in bellek düzeninin vector’dan temelden farklı olmasından kaynaklanıyor, silme işleminin “daha güvenli” yazılmış olmasından değil.

Geçersiz Yineleyici Neden Okunmaz

erase’in döndürdüğü yineleyiciyi kullanmak yerine, silinen elemana ait eski yineleyiciyi doğrudan okumak isteyen bir kod satırı derlenir ama çalıştırılamaz — bu, derleyicinin yakalayamayacağı bir tanımsız davranıştır. Aşağıdaki blok o satırı yalnız yorum olarak taşır ve neden hiç çalıştırılmadığını açıklar:

// gecersiz-yineleyici-yorumu.cpp — gecersiz yineleyiciyi okumamanin gerekcesi
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> sayilar{10, 20, 30, 40};
    auto it = sayilar.begin() + 1;

    sayilar.erase(it);

    // Asagidaki satir tanimsiz davranistir ve bu yuzden hic calistirilmaz:
    // std::printf("%d\n", *it);

    std::printf("erase sonrasi eleman sayisi %zu\n", sayilar.size());
    return 0;
}
erase sonrasi eleman sayisi 3

Derleyici *it satırını reddetmez, çünkü it’in geçerliliği bir tip denetimi değil, bir çalışma zamanı sözleşmesidir. Standart bu sözleşmeyi ihlal eden kodun davranışını tanımlamadığı için, güvenli olan tek yol o satırı hiç yazmamak ya da hiç çalıştırmamaktır — bu kursun her dersinde uygulanan kural budur. Bu, statik analiz araçlarının bazılarının neden bu sınıf hataları tam yakalayamadığını da açıklar: it’in geçerliliği it’in tipinde durmaz, it’i üreten kapsayıcının geçirdiği işlem geçmişinde durur — bir araç bu geçmişi izlemedikçe, geçerli görünen bir yineleyici çağrısının aslında geçersizleşmiş bir konuma eriştiğini göremez.

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

Aynı erase çağrısı, hedef aldığı yineleyiciyle sınırlıdır — kalan yineleyicilere ne olduğu kapsayıcının türüne, işlemin kendisine değil, bağlıdır. Dört elemanlı bir std::list üzerinde ortadaki iki elemanı silip uçlardaki yineleyicileri yeniden okuyalım:

#include <cstdio>
#include <iterator>
#include <list>

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

    auto ilk = sayilar.begin();
    auto son = std::prev(sayilar.end());
    auto orta_bir = std::next(sayilar.begin());
    auto orta_iki = std::next(sayilar.begin(), 2);

    sayilar.erase(orta_bir);
    sayilar.erase(orta_iki);

    std::printf("%d %d\n", *ilk, *son);
    return 0;
}
1 4

erase(orta_bir) yalnız orta_bir’in gösterdiği düğümü geçersizleştirir; ilk ve son başka düğümlere bağlıdır ve etkilenmez. İkinci erase çağrısı da aynı kuralı izler: orta_iki kendi düğümüyle birlikte geçersiz olur, ama bu, zaten silinmiş orta_bir’e bir şey katmaz — ilk ve son işlemden önceki değerlerini aynen göstermeye devam eder. Aynı senaryoyu bir std::vector ile kursaydık sonuç başka olurdu: eleman silindiğinde ardından gelen her eleman bir konum kaydırılır, dolayısıyla son gibi sondan alınmış bir yineleyici de geçersizleşirdi. Ölçümün sınıfını belirleyen şey silinen eleman sayısı ya da yineleyicinin nereden alındığı değil, kapsayıcının bellek düzenidir: düğüm tabanlı bir yapıda yalnız dokunulan düğüm geçersizleşirken, bitişik bellekte bir silme kendisinden sonraki her konumu etkiler.

Özet

  • std::vector::erase, silinen elemana ait yineleyiciyi geçersizleştirir; onu erase’den sonra okumak belirsiz davranıştır ve hiç çalıştırılmaz. Çağrının döndürdüğü yeni yineleyici ise geçerlidir ve kaldırılan elemandan sonraki konumu gösterir. Sınırlayıcı ölçüm: aynı işlem bir std::list’te yalnız hedef aldığı düğümü geçersizleştirir; dokunulmayan yineleyiciler aynen kalır — geçerliliği belirleyen işlem değil kapsayıcının bellek düzenidir.
  • std::list::erase ve std::map::erase/insert, düğüm tabanlı yapılarında yalnız dokunulan düğümün yineleyicisini geçersizleştirir; diğer tüm yineleyiciler aynen kalır — bellek bitişik olmadığı için eleman kaydırma hiç gerçekleşmez.
  • Yineleyiciler girdi, ileri, çift yönlü ve rastgele erişim kategorilerine ayrılır; kategori, hangi işlemlerin desteklendiğini belirler.
  • Bu dersin merkezi ölçümünün sınıfı belirsiz: erase edilen bir elemana ait yineleyicinin durumu standart tarafından tanımlanmaz, tek güvence onu bir daha okumamaktır.
  • end() her zaman tanımlı ve karşılaştırılabilir bir değerdir; bir kapsayıcının boş olması begin() == end() ile ifade edilir. Bu, geçersizleşmiş bir yineleyicinin belirsiz durumundan ayrı bir şeydir — biri “burada eleman yok” der, öbürü hiçbir garanti taşımaz.
  • std::vector::push_back, kapasite dolmuşsa yalnız eklenen elemanı değil, önceden alınmış her yineleyiciyi geçersizleştirir; kapasite yeterliyse hiçbir yineleyici etkilenmez. Bu yüzden bir vector üzerinde döngü içinde push_back çağırırken önceden alınmış yineleyicilere güvenmemek gerekir.

Sonraki Adım

Bu ders tek bir yineleyicinin geçerliliğine odaklandı. Sonraki ders bir kapsayıcının tüm elemanları üzerinde çalışan standart algoritmalara geçiyor: std::sort bir kapsayıcıyı yerinde nasıl değiştiriyor ve bu değişiklik hangi yineleyicileri etkiliyor?

İ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