Ders 05 / 11
Kapsayıcılar
map'in operator[] ile okumasının anahtar yoksa eleman eklediğinin, find ve contains'in kaynağı aynen bıraktığının ölçümü.
Yayın Güncelleme
Bu derste
- std::map'te operator[] ile okumanın anahtar yoksa haritaya sıfırla kurulmuş bir eleman eklediğini ölçer
- find ve contains'in aynı haritayı aynen bıraktığını gösterir
- Sıralı, ilişkisel ve sırasız kapsayıcılar arasında karmaşıklığa göre seçim yapar
- const bir haritada operator[] çağrısının neden derlenmediğini açıklar
İçindekiler
Önceki ders Kopyalanamaz ve Taşınamaz Tipler’de bir sınıfın kendi taşıyan
yapıcısını yazarak taşınmış kaynağa hangi boş durumu bırakacağına karar
verdiğini gösterdi — Tutamac’ın numarası -1 oluyordu, çünkü taşıyan
yapıcı öyle yazılmıştı. Bu ders standart kütüphanenin hazır kapsayıcılarına
geçiyor ve aynı soruyu bu kez bir okuma işlemine soruyor: stok["armut"]
yazmak, bir değeri okumak gibi görünüyor. Ama "armut" anahtarı haritada
yoksa bu satır kaynağa ne bırakıyor?
operator[] ile Okumak Aslında Yazmaktır
std::map<std::string, int>::operator[], anahtar haritada zaten varsa ona
bağlı değeri döndürür. Anahtar yoksa farklı bir şey yapar: değeri sıfırla
kurulmuş yeni bir eleman ekler ve o elemana bir referans döndürür. Bu,
imzasından çıkarılamayan bir yan etkidir — [] köşeli parantezi çoğu
programcıya “sorgula” der, ama map için anlamı “varsa getir, yoksa kur”dur.
// kapsayici-okuma.cpp — operator[] ile okuma haritaya eleman ekler mi
#include <cstdio>
#include <map>
#include <string>
int main() {
std::map<std::string, int> stok{{"elma", 4}};
std::size_t once = stok.size();
int adet = stok["armut"];
(void)adet;
std::printf("eleman sayisi degisti mi %s\n",
(stok.size() != once) ? "evet" : "hayir");
std::printf("armut degeri %d\n", stok["armut"]);
return 0;
}
eleman sayisi degisti mi evet armut degeri 0
KT1. stok["armut"] çağrısı, "armut" haritada yokken haritayı
değiştirdi: eleman sayısı bir arttı ve yeni elemanın değeri int’in
varsayılan değeri olan 0 oldu. adet değişkenine atanan değer bir okuma
gibi görünse de, o okumanın kendisi haritaya yazdı. Bu ifadenin sınıfı
değişti — kaynak hâlâ bir değer taşıyor ve standart bu değeri söylüyor
(boş harita değil, bir elemanlı harita), ama değer işlemden öncekinden
farklı.
find ve contains: Aynı Soruyu Eklemeden Sormak
Bir anahtarın haritada olup olmadığını öğrenmenin operator[]’i çağırmayan
iki yolu vardır. find, anahtarı bulursa o elemana bir yineleyici, bulamazsa
end() döndürür — hiçbir durumda eleman eklemez. contains, C++20 ile gelen
daha doğrudan yol; yalnız bir bool döndürür.
// kapsayici-sorgu.cpp — find ve contains haritayi degistirir mi
#include <cstdio>
#include <map>
#include <string>
int main() {
std::map<std::string, int> stok{{"elma", 4}};
auto once = stok;
auto it = stok.find("armut");
bool var = stok.contains("kiraz");
std::printf("find sonucu %s\n", (it == stok.end()) ? "bulunamadi" : "bulundu");
std::printf("contains sonucu %s\n", var ? "evet" : "hayir");
std::printf("harita degisti mi %s\n", (stok != once) ? "evet" : "hayir");
return 0;
}
find sonucu bulunamadi contains sonucu hayir harita degisti mi hayir
KT2. find("armut") ve contains("kiraz") ikisi de haritada olmayan
anahtarları sorguladı ve ikisi de haritayı aynen bıraktı: stok, işlem
öncesindeki kopyasıyla hâlâ eşit. find’ın döndürdüğü end() yineleyicisi
“burada yok” demenin bir yolu, eklemenin bir tetikleyicisi değil. Bir haritada
bir anahtarın varlığını yalnız denetlemek isteyen kod operator[] yerine
find ya da contains kullanmalı — aksi hâlde sorgu, sorguladığı yapıyı
değiştirir.
Kapsayıcı Aileleri ve Seçim Ölçütü
Standart kütüphane kapsayıcıları üç aileye ayrılır. Sıralı kapsayıcılar
(vector, deque, list, array) elemanın konumunu eklenme sırasından
alır — burada “sıralı” büyüklüğe göre dizilmiş anlamına gelmez, elemanın yeri
programcının onu koyduğu yerdir. vector bitişik bellekte durur ve sona
ekleme ortalama sabit zamanlıdır; sona erişim indeksle sabit zamanlıdır.
list çift yönlü bağlı bir listedir; ortadan ekleme sabit zamanlıdır ama
indeksle erişim yoktur, baştan gezilir.
İlişkisel kapsayıcılar (map, set) elemanları bir anahtara göre
sıralı tutar; genellikle dengeli bir ikili arama ağacıyla gerçeklenir ve
arama, ekleme, silme O(log n) karmaşıklıktadır. Sırasız kapsayıcılar
(unordered_map, unordered_set) elemanları bir özet tablosunda tutar;
ortalama durumda arama ve ekleme O(1)’dir, ama en kötü durumda bütün
elemanlar aynı kovaya düşerse O(n)’e düşer. Seçim, hangi işlemin sık
yapılacağına bağlıdır: sık sıralı gezinme gerekiyorsa map, yalnız hızlı
arama gerekiyor ve sıra önemsizse unordered_map tercih edilir.
// aile-secimi.cpp — sirali, iliskisel ve sirasiz kapsayicidan okuma
#include <cstdio>
#include <map>
#include <unordered_map>
#include <vector>
int main() {
std::vector<int> sirali{30, 10, 20};
std::map<int, int> iliskisel{{1, 10}, {2, 20}};
std::unordered_map<int, int> sirasiz{{1, 10}, {2, 20}};
std::printf("vector ilk eleman %d\n", sirali.front());
std::printf("map en kucuk anahtar %d\n", iliskisel.begin()->first);
std::printf("unordered_map arama %d\n", sirasiz.at(1));
return 0;
}
vector ilk eleman 30 map en kucuk anahtar 1 unordered_map arama 10
KT3. Bu üç okuma da bir kapsayıcıyı aynen bıraktı: front(),
yineleyici üzerinden first okuması ve at() hiçbiri kaynağa yazmadı.
unordered_map’in elemanlarını hangi sırada tuttuğu burada hiç
sorgulanmadı ve sorgulanmayacak — sırasız kapsayıcının dolaşma sırası
gerçeklemeye bağlıdır ve bu kursta hiç basılmaz. map’te begin()->first
her koşumda 1 verir çünkü map anahtara göre sıralıdır; aynı çağrı bir
unordered_map üzerinde yapılsaydı sonuç güvenilir olmazdı.
at(): Sınır Denetimi Olan Erişim
operator[]’in bir sorunu daha var: const bir haritada kullanılamaz,
çünkü anahtar yoksa eleman ekleyebilme ihtimali onu değiştirici bir işlem
yapar. at() bu sorunu ortadan kaldırır — anahtar yoksa eleman eklemez,
bunun yerine std::out_of_range fırlatır ve const bir haritada da
kullanılabilir.
// at-erisimi.cpp — at() sinir disi anahtarda ne yapar
#include <cstdio>
#include <map>
#include <stdexcept>
#include <string>
int main() {
const std::map<std::string, int> stok{{"elma", 4}};
try {
int deger = stok.at("armut");
(void)deger;
std::printf("yakalandi mi hayir\n");
} catch (const std::out_of_range&) {
std::printf("yakalandi mi evet\n");
}
std::printf("eleman sayisi %zu\n", stok.size());
return 0;
}
yakalandi mi evet eleman sayisi 1
KT4. stok.at("armut") bir istisna fırlattı ve bu ders yalnız
yakalandığını bastı — istisnanın what() metnini basmadı, çünkü o
metnin kesin biçimi kütüphane uygulamasına bağlıdır ve dersin ölçtüğü şey
değil. Fırlatmadan sonra stok’un eleman sayısı hâlâ 1: harita aynen
kaldı. Bu, at()’i const bir kapsayıcı üzerinde güvenle kullanılabilir
kılan şey — sınır dışı bir anahtar isteği kaynağı hiç değiştirmiyor, yalnız
bunu bir istisnayla bildiriyor.
insert ve emplace: Var Olan Anahtara Dokunmama Kuralı
operator[] yalnız anahtar yoksa eleman ekleyip döndürmüyor; anahtar zaten
varsa değeri değiştiriyor. insert ve emplace bunun tam tersi bir
garanti taşır: anahtar zaten varsa hiçbir şeyi değiştirmezler ve mevcut
elemana dokunmadıklarını bir bool ile bildirirler.
// insert-emplace.cpp — anahtar zaten varken insert kaynagi degistirir mi
#include <cstdio>
#include <map>
#include <string>
int main() {
std::map<std::string, int> stok{{"elma", 4}};
auto sonuc = stok.insert({"elma", 99});
bool eklendi = sonuc.second;
std::printf("eklendi mi %s\n", eklendi ? "evet" : "hayir");
std::printf("elma degeri %d\n", stok.at("elma"));
return 0;
}
eklendi mi hayir elma degeri 4
KT5. insert({"elma", 99}) çağrısı "elma" anahtarı haritada zaten
varken çalıştı ve sonuc.second false döndü — hiçbir ekleme olmadı.
elma değeri hâlâ 4: harita bu çağrıdan sonra aynen kaldı, 99
değeri hiçbir zaman yazılmadı. Bu, operator[] ile taban çizgide bir fark
yaratır: stok["elma"] = 99 yazılsaydı değer koşulsuz üzerine yazılır ve
sınıf değişti olurdu. insert’in bu geri çevirme davranışı, bir haritayı
yalnız yoksa doldurmak isteyen kodun operator[] yerine insert ya da
emplace seçmesinin gerekçesidir — emplace aynı garantiyi taşır, farkı
elemanı bir geçici nesne kurmadan doğrudan kapsayıcının belleğinde inşa
etmesidir.
erase’in Karmaşıklığı Kapsayıcıya Göre Değişir
Bir elemanı silmek her kapsayıcıda aynı işi yapmaz. std::vector::erase
silinen konumdan sonraki her elemanı bir konum sola kaydırır — bu, silinen
konum başa ne kadar yakınsa o kadar çok eleman taşınacağı anlamına gelir ve
karmaşıklığı O(n)dir. std::list::erase bitişik bellek tutmadığı için
kaydırma yapmaz; yalnız silinen düğümün komşularının bağlantısını günceller
ve karmaşıklığı O(1)dir. std::map::erase ise bir anahtara göre arama
yapıp ardından ağaçtaki bir düğümü kaldırır, toplamda O(log n).
// erase-karmasikligi.cpp — vector erase kalan elemanlari kaydirir mi
#include <cstdio>
#include <vector>
int main() {
std::vector<int> sirali{10, 20, 30, 40};
sirali.erase(sirali.begin());
std::printf("eleman sayisi %zu\n", sirali.size());
std::printf("ilk eleman %d\n", sirali.front());
return 0;
}
eleman sayisi 3 ilk eleman 20
KT6. sirali.erase(sirali.begin()) ilk elemanı sildi ve kalan üç eleman
bir konum sola kaydı — front() artık eskiden ikinci sırada duran 20’yi
veriyor. Kaynak bu işlemden sonra değişti: yalnız eleman sayısı bir
azalmadı, kalan elemanların bellekteki konumu da değişti. Bir std::list
üzerinde aynı çağrı yapılsaydı sonuç sınıf olarak yine değişti olurdu
— eleman sayısı yine bir azalırdı — ama kaydırma hiç gerçekleşmezdi;
kalmayan elemanların adresleri sabit kalırdı. Bu dersin sınıflandırması
(aynen/değişti/boş/belirsiz) bir işlemin ne kadar iş yaptığını değil,
kaynağı ne durumda bıraktığını ölçtüğü için, vector ve list üzerindeki
bu iki erase çağrısı aynı sınıfa düşer; karmaşıklık farkı ayrı bir eksendir
ve seçim yaparken karmaşıklık ile sınıf birlikte değerlendirilir, biri
diğerinin yerine geçmez.
Sınır Dışı İndeksleme: Tanımsız Davranış ve Tanımlı Karşılığı
std::vector::operator[], verdiği indeksin kapsayıcının sınırları içinde
olduğunu hiç denetlemez. sirali[10] çağrısı sirali’nin boyutu 3
olduğunda tanımsız davranıştır: çalışma zamanı bir çökme üretebilir, çöp
bir değer döndürebilir ya da hiçbir hata vermeden “çalışabilir” — üçü de eşit
derecede olasıdır ve bu kurs hiçbirini göstermez, çünkü tanımsız davranışın
çıktısı standarda göre tanımlı değildir ve doğrulanamaz. std::vector::at()
aynı isteğe tanımlı bir karşılık verir: indeks sınır dışıysa
std::out_of_range fırlatır, sınır içindeyse operator[] ile aynı elemana
erişir. Fark performanstadır — at() her çağrıda bir sınır denetimi
yapar, operator[] yapmaz — ama bu fark yalnız sınır içindeki erişimlerde
görünür bir maliyettir; sınır dışı bir erişimde operator[]’in “maliyetsiz”
olması, kaynağın ne durumda kaldığının artık tanımsız olması pahasınadır.
Bir indeksin geçerliliğinden emin olunamayan her yerde at() tercih edilir;
indeksin geçerliliği çağıran tarafından zaten garanti edildiği sıkı döngülerde
operator[]’in denetimsiz hızı kabul edilir.
Sınırlayıcı Ölçüm
const std::map<std::string, int>& stok üzerinde stok["armut"] yazmak
derlenmez: operator[]’in const olmayan bir aşırı yüklemesi vardır
ve const bir nesne üzerinde çağrılamaz, çünkü işlecin gövdesi anahtar
yoksa eleman ekleyebilir — bu, const sözleşmesiyle çelişir. Aşağıdaki
blok bu satırı yorum olarak taşır ve onun yerine derlenen sürümü,
const bir haritanın yalnız find, at ve contains ile okunabildiğini
gösterir:
// const-harita.cpp — const haritada operator[] neden kullanilamaz
#include <cstdio>
#include <map>
#include <string>
int main() {
const std::map<std::string, int> stok{{"elma", 4}};
// Asagidaki satir derlenmez, cunku operator[] const olmayan bir islev:
// int adet = stok["armut"];
auto it = stok.find("elma");
std::printf("elma bulundu mu %s\n", (it != stok.end()) ? "evet" : "hayir");
std::printf("eleman sayisi %zu\n", stok.size());
return 0;
}
elma bulundu mu evet eleman sayisi 1
Derleyicinin bu satırı reddetmesi bir kısıtlama değil, bir güvencedir:
const bir haritanın yalnız sorgulanabileceğini, hiçbir okumanın onu
değiştiremeyeceğini derleme anında zorunlu kılar. Bu dersin ölçtüğü işlem
— değiştirilebilir bir haritada operator[] ile okuma — bu güvenceyi
taşımaz; kaynağa yazan yol açıktır ve derleyici onu kapatmaz.
Özet
std::map’teoperator[]ile okuma, anahtar yoksa haritaya sıfırla kurulmuş bir eleman ekler; bu dersin ölçtüğü işlemin sınıfı değişti.findvecontains, anahtarı ararken haritaya hiç yazmaz; kaynak aynen kalır.- Sıralı (
vector,deque,list,array), ilişkisel (map,set) ve sırasız (unordered_map,unordered_set) kapsayıcılar farklı karmaşıklık garantileri taşır (eraseörneğinvector’deO(n),list’teO(1),map’teO(log n)); ama bu fark sınıflandırmayı etkilemez, hepsi kaynağı değişti durumuna taşır. insertveemplace, anahtar zaten varsa hiçbir şey yazmaz — kaynak aynen kalır;operator[]ile yazmak ise koşulsuz üzerine yazar ve sınıf değişti olur.at(), anahtar yoksa istisna fırlatır ve haritayı hiç değiştirmez, bu yüzdenconstbir kapsayıcı üzerinde de kullanılabilir; buna karşınconstbir haritadaoperator[]çağrısı derlenmez — derleyici, kaynağı değiştirebilecek bir yoluconstsözleşmesiyle baştan kapatır.vector::operator[]sınır dışı bir indekste tanımsız davranışa girer;vector::at()aynı isteğe tanımlı bir istisna fırlatarak karşılık verir — ikisi de kaynağı okumaktan öteye gitmez, fark yalnız sınır dışı isteğe verilen yanıtın tanımlı olup olmadığındadır.
Sonraki Adım
Bu ders bir kapsayıcının bütününe ne olduğunu ölçtü. Sonraki ders sınırı bir kapsayıcının içindeki bir konuma indiriyor: bir yineleyici, işaret ettiği elemanı silme işleminden sonra hâlâ okunabilir mi, yoksa o konuma dokunmak tanımsız davranışa mı giriyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.