Ders 08 / 11
Sil–Kaldır Deyimi
std::remove sonrası vector'ün kuyruğunda kalan elemanların belirsiz değer taşıdığının ve erase ile bu kuyruğun gerçekten kaldırıldığının ölçümü.
Yayın Güncelleme
Bu derste
- std::remove'un elemanları gerçekten silmediğini, yalnız öne taşıdığını gösterir
- remove sonrası vector'ün kuyruğundaki değerlerin belirsiz olduğunu ölçer
- erase ile birlikte kullanıldığında kuyruğun gerçekten kaldırıldığını gösterir
- C++20'nin std::erase serbest işlevinin aynı deyimi tek satıra indirdiğini tanır
İçindekiler
Önceki ders std::sort’un bir vector’ün elemanlarını yerinde yeniden
sıraladığını, bunun kaynağı değiştirdiğini gösterdi. Sıralama sırasında
kapsayıcının eleman sayısı hiç değişmedi. Bu ders eleman sayısını
azaltan bir işleme geçiyor, ama beklenmedik bir ayrıntıyla: adında
“kaldır” geçen std::remove, bir vector’ün boyutunu değiştirmez. Peki ne
yapar?
Bu ayrıntı yeni başlayanların en çok şaşırdığı noktalardan biridir: bir işlev
adının vaat ettiği şeyle gerçekte yaptığı şey her zaman birebir örtüşmez.
std::remove bir algoritmadır ve algoritmalar yalnız yineleyici çiftleriyle
çalışır — kapsayıcının kendisine erişimleri yoktur, dolayısıyla kapsayıcının
boyutunu küçültme yetkileri de yoktur. Bu ders, bu sınırın nereden geldiğini
ve gerçek kaldırmanın nasıl tamamlandığını adım adım gösteriyor.
std::remove: Kaldırmayan Bir “Kaldırma”
std::remove, verilen aralıktaki elemanları gezer; aranan değere eşit
olmayanları aralığın başına doğru taşır ve yeni “geçerli” bölgenin bittiği
yeri gösteren bir yineleyici döndürür. Kapsayıcının boyutunu değiştirmez —
değiştiremez de, çünkü bir yineleyici çiftiyle çalışır, kapsayıcının
kendisiyle değil.
// remove-siz-erase.cpp — std::remove tek basina ne yapiyor
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> sayilar{1, 7, 2, 7, 3, 7};
std::size_t once = sayilar.size();
auto yeni_son = std::remove(sayilar.begin(), sayilar.end(), 7);
std::printf("boyut degisti mi %s\n",
(sayilar.size() != once) ? "evet" : "hayir");
std::printf("gecerli bolgenin uzunlugu %ld\n",
static_cast<long>(std::distance(sayilar.begin(), yeni_son)));
return 0;
}
boyut degisti mi hayir gecerli bolgenin uzunlugu 3
KT19. std::remove çağrısından sonra sayilar.size() hâlâ 6 —
kapsayıcının boyutu değişmedi. yeni_son yineleyicisi geçerli bölgenin üç
eleman uzunluğunda olduğunu gösteriyor: 1, 2, 3 değerleri öne taşındı.
Kalan üç konum (yeni_son’dan sayilar.end()’e kadar) hâlâ var, ama
içerikleri artık anlamlı değil. Bu ilk adımın sınıfı değişti: dizinin ilk
üç konumu tanımlı bir sonuca (1, 2, 3) sahip.
Kuyrukta Kalan Değerler: Belirsiz
yeni_son’dan sayilar.end()’e kadar olan konumlar std::remove’un
gerçekleştirme ayrıntısına bağlıdır — standart bu konumlardaki değerlerin
ne olacağını tanımlamaz, yalnız “belirlenmemiş bir değer” taşıyacaklarını
söyler. Bu ders o konumları okumuyor.
// remove-kuyrugu-okumuyor.cpp — gecerli bolge disina neden bakilmiyor
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> sayilar{1, 7, 2, 7, 3, 7};
auto yeni_son = std::remove(sayilar.begin(), sayilar.end(), 7);
std::printf("gecerli bolge:");
for (auto it = sayilar.begin(); it != yeni_son; ++it) {
std::printf(" %d", *it);
}
std::printf("\n");
// yeni_son'dan sayilar.end()'e kadar olan degerler okunmuyor,
// cunku standart bu bolgedeki degerleri tanimlamiyor.
return 0;
}
gecerli bolge: 1 2 3
KT20. Bu ders kuyruktaki üç konumu (yeni_son’dan sonrası) hiçbir
printf çağrısında okumadı — bu, tanımsız davranıştan kaçınmak için değil
(kuyruğu okumak tanımsız davranış değildir, elemanlar hâlâ var ve
geçerlidir), ama belirsiz bir değeri kesin bir sayıymış gibi göstermemek
için. Standart bu konumların hangi değerleri taşıyacağını tanımlamadığından,
bu değerleri basmak beş yıl sonra farklı bir standart kütüphane
gerçeklemesinde farklı bir çıktı üretebilirdi. Bu işlemin sınıfı belirsiz:
kuyruk konumlarının içeriği standart tarafından garanti edilmez.
erase ile Kuyruğu Gerçekten Kaldırmak
std::remove’un döndürdüğü yineleyiciyi kapsayıcının kendi erase üye
işlevine vermek, kuyruğu gerçekten kaldırır ve kapsayıcının boyutunu
küçültür. Bu iki adımlı örüntüye sil–kaldır deyimi denir.
// remove-erase-deyimi.cpp — iki adimda gercek kaldirma
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> sayilar{1, 7, 2, 7, 3, 7};
auto yeni_son = std::remove(sayilar.begin(), sayilar.end(), 7);
sayilar.erase(yeni_son, sayilar.end());
std::printf("boyut %zu\n", sayilar.size());
std::printf("icerik:");
for (int deger : sayilar) {
std::printf(" %d", deger);
}
std::printf("\n");
return 0;
}
boyut 3 icerik: 1 2 3
KT21. erase(yeni_son, sayilar.end()) çağrısından sonra sayilar.size()
3’e düştü ve içerik yalnız 1, 2, 3. Bu adımın sınıfı değişti: hem
boyut hem içerik, standardın tanımladığı biçimde kesin bir sonuca ulaştı.
İki adımı birleştirdiğimizde ortaya çıkan tablo şu: remove tek başına
kaynağı değişti durumuna sokar ama kuyruğu belirsiz bırakır; erase
eklendiğinde kuyruk da kaldırılır ve kaynak yeniden tam anlamıyla
değişti olur — artık okunmayan bir belirsiz bölge kalmaz. Bu iki adımın
neden ayrı tutulduğu da bu noktada netleşir: remove yalnız bir yineleyici
çiftiyle çalıştığı için kapsayıcının kendisini küçültemez; kapsayıcıyı
küçültme yetkisi yalnız o kapsayıcının kendi erase üye işlevinde vardır.
İki ayrı adım, iki ayrı sorumluluğu yansıtır: biri elemanları taşımayı,
diğeri belleği geri vermeyi üstlenir.
C++20: std::erase Serbest İşlevi
C++20, sık kullanılan bu iki adımı tek bir çağrıya indiren serbest işlevler
ekledi: std::erase ve std::erase_if. Bu işlevler <vector> başlığında
tanımlıdır ve remove+erase çiftini kapsayıcıya özel bir üye işlev
gerektirmeden uygular.
// std-erase-serbest-islev.cpp — tek cagrida sil-kaldir
#include <cstdio>
#include <vector>
int main() {
std::vector<int> sayilar{1, 7, 2, 7, 3, 7};
std::erase(sayilar, 7);
std::printf("boyut %zu\n", sayilar.size());
std::printf("icerik:");
for (int deger : sayilar) {
std::printf(" %d", deger);
}
std::printf("\n");
return 0;
}
boyut 3 icerik: 1 2 3
KT22. std::erase(sayilar, 7) tek satırda önceki iki adımın (KT19 ve
KT21) yaptığı işi yaptı; sonuç aynı: boyut 3, içerik 1, 2, 3. Bu
işlemin sınıfı da değişti — belirsiz bir ara duruma bu kez hiç
uğranmadı, çünkü kuyruk taşıma ve gerçek kaldırma tek bir işlevin içinde,
kullanıcıya hiçbir ara yineleyici göstermeden gerçekleşti. Eski
remove+erase deyimi hâlâ derlenir ve C++20 öncesi kodda hâlâ görülür;
std::erase, aynı sonucu daha az kod satırıyla ve ara belirsiz durumu
gizleyerek verir.
Bu Kurs Saymaz: Ölçümün Ekseni Kaç Elemanın Silindiği Değildir
“Trait’ler, Genellikler ve Hata Yönetimi” dersinin (Rust müfredatındaki
rust-soyutlama kursu) ekseni bir gövdenin kaç kez koştuğudur — bir
yüklemin ya da bir kapanışın çağrı sayısını ölçer. Bu ders farklı bir
eksende duruyor: std::erase_if’in kaç elemanı kaldırdığı bu dersin konusu
değil. sayilar.size()’ın 6’dan 3’e düşmesi bir sayaç değil,
kapsayıcının şu andaki durumunu gösteren tek bir değer — tıpkı bir önceki
derste std::count’un döndürdüğü sayının bir çağrı sayısı değil bir sonuç
olması gibi. Bu dersin ölçtüğü şey kaldırılan eleman sayısı değil,
kaldırma sonrası kaynağın hangi sınıfa (belirsiz mi, değişti mi)
düştüğüdür.
std::remove’un Karmaşıklığı: Eşleşme Sayısından Bağımsız
std::remove ve std::remove_if, aralığı baştan sona tek geçişte
tarar; karmaşıklıkları O(n)’dir ve bu, aralıktaki kaç elemanın aranan
değere eşit çıktığından bağımsızdır. Algoritma hiçbir elemanı iki kez
ziyaret etmez: ya elemanı öne taşır ya da üzerinden atlar, sonra bir
sonraki konuma geçer.
// remove-karmasikligi.cpp — tek gecis, eslesme sayisindan bagimsiz
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> hicbiri_eslesmez{1, 2, 3, 4, 5};
std::vector<int> hepsi_eslesir{9, 9, 9, 9, 9};
auto son1 = std::remove(hicbiri_eslesmez.begin(),
hicbiri_eslesmez.end(), 9);
auto son2 = std::remove(hepsi_eslesir.begin(),
hepsi_eslesir.end(), 9);
std::printf("hicbiri_eslesmez gecerli uzunluk %ld\n",
static_cast<long>(std::distance(hicbiri_eslesmez.begin(), son1)));
std::printf("hepsi_eslesir gecerli uzunluk %ld\n",
static_cast<long>(std::distance(hepsi_eslesir.begin(), son2)));
return 0;
}
hicbiri_eslesmez gecerli uzunluk 5 hepsi_eslesir gecerli uzunluk 0
KT23. İki çağrı da aynı boyuttaki (5 elemanlı) aralığı tek geçişte
tarıyor; ilkinde hiçbir eleman eşleşmediği için geçerli bölge tüm aralığı
kaplıyor (5), ikincisinde her eleman eşleştiği için geçerli bölge boşalıyor
(0). İkisinin de çalışma süresi aynı büyüklük mertebesindedir — std::remove
kaç eşleşme bulduğuna göre farklı bir yol izlemez, her elemanı tam olarak bir
kez ziyaret eder. Bu, algoritmanın karmaşıklığının girdi içeriğine değil
girdi uzunluğuna bağlı olduğunu gösteren bir örnek.
std::list::remove: Deyime İhtiyaç Duymayan Bir Üye İşlev
std::vector için remove+erase deyimi gerekliyken, std::list kendi
remove ve remove_if üye işlevlerini sağlar ve bunlar deyime ihtiyaç
duymaz — çağrıldıkları anda eşleşen düğümleri gerçekten bağlı listeden
çıkarırlar, ayrı bir erase çağrısı gerekmez.
// list-remove-uye-islev.cpp — list kendi remove'unu saglar
#include <cstdio>
#include <list>
int main() {
std::list<int> sayilar{1, 7, 2, 7, 3, 7};
sayilar.remove(7);
std::printf("boyut %zu\n", sayilar.size());
std::printf("icerik:");
for (int deger : sayilar) {
std::printf(" %d", deger);
}
std::printf("\n");
return 0;
}
boyut 3 icerik: 1 2 3
KT24. sayilar.remove(7) tek başına, ayrı bir erase çağrısı olmadan,
vector örneğindeki iki adımlı deyimin ulaştığı sonuca ulaştı: boyut 3,
içerik 1, 2, 3. Bu işlemin sınıfı değişti. Fark, mekanizmada:
std::list düğümleri bağlı bir yapıda tuttuğu için bir düğümü kaldırmak
komşu düğümleri kaydırmayı gerektirmez; remove üye işlevi bu kaldırmayı
doğrudan yapabilir. std::vector ise elemanları bitişik bellekte tuttuğu
için genel std::algorithm’un elinde kapsayıcının boyutunu değiştirecek bir
yetki yoktur — bu yüzden std::remove yalnız taşıma yapar, gerçek kaldırma
kapsayıcıya özel erase’e kalır.
std::remove_if ve std::erase_if: Bir Yükleme Göre Kaldırma
std::remove yalnız bir değere eşitliği ölçer. Kaldırma kararı bir koşula
bağlıysa std::remove_if kullanılır; ilk argümanı bir değer değil, bir
yüklemdir. Deyim aynı kalır: remove_if’in döndürdüğü yineleyici erase‘e
verilir, ya da C++20’de tek çağrıda std::erase_if kullanılır.
// remove-if-erase-if.cpp — yukleme gore kaldirma
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> sayilar{4, 15, 8, 23, 6, 42};
std::erase_if(sayilar, [](int deger) { return deger > 10; });
std::printf("boyut %zu\n", sayilar.size());
std::printf("icerik:");
for (int deger : sayilar) {
std::printf(" %d", deger);
}
std::printf("\n");
return 0;
}
boyut 3 icerik: 4 8 6
KT25. std::erase_if, yüklemi sağlayan (10’dan büyük olan) üç
elemanı (15, 23, 42) kaldırdı; kalan üç eleman girdi sırasını korudu
(4, 8, 6). Bu işlemin sınıfı değişti: boyut 6’dan 3’e düştü ve
standart, hangi elemanların kalacağını (yüklemi sağlamayanlar) ve bunların
göreli sırasının korunacağını tanımlar. remove_if+erase çiftiyle
erase_if arasındaki ilişki, remove+erase ile erase arasındaki
ilişkinin aynısı: ikisi de aynı iki adımı uyguluyor, erase_if yalnız
o adımları tek çağrıda gizliyor ve ara belirsiz kuyruğu kullanıcıya hiç
göstermiyor.
Kuyruk Okunursa: Çökme Değil, Tanımsız Değer
std::remove’un döndürdüğü yeni_son yineleyicisinden sayilar.end()’e
kadar olan konumları döngüyle okuyup basmaya çalışan bir kod derlenir ve
çalışır — burada bir derleme hatası ya da bir çökme yoktur. Sorun
çalışma zamanında da görünmez: program bir sayı basar. Sorun, o sayının
hangi standart kütüphane gerçeklemesinde hangi değeri vereceğinin
tanımsız olmasıdır.
// kuyruk-okunursa.cpp — kuyrugu okumak neden yanlis, cokme degil
#include <algorithm>
#include <cstdio>
#include <vector>
int main() {
std::vector<int> sayilar{1, 7, 2, 7, 3, 7};
auto yeni_son = std::remove(sayilar.begin(), sayilar.end(), 7);
std::printf("gecerli bolge uzunlugu %ld\n",
static_cast<long>(std::distance(sayilar.begin(), yeni_son)));
// Kuyruktaki degerler bilerek basilmiyor: standart onlari tanimlamiyor,
// bu yuzden bu ders o degerlere dayanan hicbir cikti uretmiyor.
return 0;
}
gecerli bolge uzunlugu 3
Bu kod çalışır ama kuyruğu okumaya çalışan bir sürümünün çıktısı bu derste hiç gösterilmiyor, çünkü o çıktı standart tarafından garanti edilmeyen bir değere dayanır. Beş yıl sonra farklı bir derleyici ya da farklı bir kütüphane gerçeklemesiyle koşulduğunda aynı satır farklı bir sayı basabilir — bu, dersin başından beri kaçındığı türden bir kanıt niteliğidir.
C++20’nin std::erase/std::erase_if serbest işlevleri yalnız std::vector
için değil, std::list, std::deque ve std::forward_list için de aşırı
yüklenmiştir. Bu, kapsayıcı türünden bağımsız ortak bir arayüz sağlar: hangi
kapsayıcıyla çalışırsanız çalışın, “bu değere/yükleme uyan elemanları kaldır”
işlemi aynı çağrıyla ifade edilir. Ancak bu ortak arayüzün altında hâlâ farklı
mekanizmalar çalışır — std::vector için içeride remove+erase deyimi
uygulanırken, std::list için doğrudan düğüm kaldırma yapılır. Arayüzün
ortaklığı, gerçekleştirme ayrıntısının aynı olduğu anlamına gelmez; bu ders
boyunca izlenen ayrım (kaynağın sınıfı: aynen, değişti, boş, belirsiz) her iki
mekanizma için de geçerli kalır, çünkü sonuç kaynağın durumuna bakılarak
değerlendirilir, uygulama ayrıntısına bakılarak değil.
Sınırlayıcı Ölçüm
remove+erase deyimi iki adımı ayrı ayrı görünür kılar ve aradaki
belirsiz kuyruğu bir an için ortaya çıkarır. Aynı işi tek çağrıda yapan
std::erase bu ara durumu hiç göstermez ve üstelik kaç eleman sildiğini
de bildirir — dönüş değeri bir yüklem çağrı sayacı değil, işlemin sonucudur:
#include <cstdio>
#include <vector>
int main() {
std::vector<int> sayilar{7, 1, 7, 2, 7};
auto silinen = std::erase(sayilar, 7);
std::printf("%zu silindi, kalan %zu\n", silinen, sayilar.size());
return 0;
}
3 silindi, kalan 2
std::erase(sayilar, 7) beş elemanlı sayilar içindeki üç 7’yi kaldırdı
ve bunu tek bir dönüş değeriyle bildirdi: silinen 3, kalan boyut 2.
Yukarıdaki kuyruk-okunursa.cpp örneğinde kuyruk hiçbir zaman okunmuyordu
çünkü içeriği belirsizdi; burada okunacak bir kuyruk hiç yok — std::erase
kaldırma ve boyut küçültme adımlarını kendi içinde uygulayıp bitirdiği için
kaynak doğrudan değişti sınıfına geçiyor, ara bir belirsiz duruma hiç
uğramıyor. Bu, yukarıda std::list::remove üye işlevi için görülen durumla
aynı kökten gelir: ikisi de deyimin iki adımını (taşı, sonra kısalt) kullanıcıya
göstermeden tek işlemde birleştirir; farkları yalnız mekanizmadadır —
std::list::remove düğüm bağlantısını değiştirirken, std::erase bir
vector üzerinde içeride remove+erase deyiminin kendisini çağırır. Adı
aynı olan std::remove (kaldırmayan) ile std::list::remove (gerçekten
kaldıran) arasındaki fark da burada tekrar görünür: ikisi de “remove” adını
taşır ama yalnız üye işlev olan gerçekten kaldırma yetkisine sahiptir.
Özet
std::remove, elemanları kaldırmaz; aranan değere eşit olmayanları öne taşır ve kapsayıcının boyutunu değiştirmez. Döndürdüğü yineleyiciden kapsayıcının sonuna kadar olan kuyruk bölgesi belirsiz değer taşır ve bu ders o bölgeyi okumaz.erase(yeni_son, end()), kuyruğu gerçekten kaldırır ve kapsayıcının boyutunu küçültür; bu adımın sınıfı değişti.- C++20’nin
std::eraseserbest işlevi, remove+erase deyimini tek satıra indirir ve aynı değişti sonucunu ara belirsiz duruma hiç uğramadan verir; kaç eleman sildiğini de dönüş değeriyle bildirir. Sınırlayıcı ölçüm tam olarak bunu doğrular:std::erase(sayilar, 7)çağrısı kuyruğa hiç uğramadan kaynağı doğrudan değişti sınıfına geçirir. - Bu dersin merkezi ölçümünün sınıfı belirsiz:
std::removesonrası kuyruktaki değerlerin içeriği standart tarafından tanımlanmaz. std::remove/std::remove_if’in karmaşıklığı O(n)’dir ve aralıktaki kaç elemanın eşleştiğinden bağımsızdır; bu ders eşleşme sayısını değil kaynağın sınıfını ölçer.std::list,remove/remove_ifüye işlevleriyle deyime ihtiyaç duymadan doğrudan kaldırma yapar;std::vector’ün genel algoritmayla neden bu yetkiye sahip olmadığı, kapsayıcıların bellek düzenindeki farktan gelir.
Sonraki Adım
Bu ders bir algoritmanın döndürdüğü yineleyiciyi kullandı ama karşılaştırma her zaman bir değere eşitlik değildi — bazı çağrılarda bir yüklem geçirildi. Bu yüklemler şimdiye kadar sıradan bir işlev işaretçisi ya da adlandırılmış bir yapı olarak düşünülebilirdi, ama pratikte çoğunlukla küçük, tek kullanımlık bir ifade olarak yazılırlar. Sonraki ders bu yüklemlerin arkasındaki mekanizmaya, lambda ifadelerine ve işlev nesnelerine geçiyor: bir lambda değeri yakaladığında o değere ne oluyor?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.