İçeriğe geç
academia.sh

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::erase serbest 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::remove sonrası 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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat