Modern C++’ın Gücünü Keşfetmek: Devrimsel Temel Özellikler

Süleyman Poyraz
8 min readJan 26, 2025

--

Önceki yazımda C++11’i önceki sürümünden ayıran özelliklerinden bahsederken şimdi bahsedeceğim üç temel özelliğinden bahsetmiştik. Bunlar type inference (tür çıkarımı), uniform initialization (birörnekli başlatma) ve smart pointers (akıllı işaretçiler) konularını ele almıştık. Bu yazıda ise bu üç hususa ve move semantics (taşıma semantiği) konusuna daha detaylı olarak gireceğiz, lambda expressions (lambda ifadeleri) ve concurrency (eşzamanlılık) gibi, bizim için muhim olan hususları daha detaylı inceleyerek geliştiricilere verimli, ifade gücü yüksek ve ölçeklenebilir kod yazma imkânı tanıyan diğer önemli özelliklere odaklanacağız.

Referanslar ve Kaynaklar

C++’ta değerler (values), özelliklerine ve yaşam sürelerine göre farklı kategorilere ayrılır. Bu ayrımlar, ifadeleri (expressions), sahiplik kavramını (ownership) ve kaynak yönetimini (resource management) anlamak için kritik öneme sahiptir. Şimdi konuyu adım adım inceleyelim:

1. C++’taki Değer Kategorileri

C++’da değerler bellekteki konumuna ve yeniden kullanılabilirliğine göre ayrılır. Bu ayrım aslında dil ile bilmemiz gereken bir özellik olduğu gibi işin özünde, C++ derleyicisinin makine kodu oluşturmasına içkin bir durum diyebilirim. Yine de kısaca bu değerler ve özelliklerinden bahsedeceğim ki bellek yönetimi hakkı ile anlaşılabilsin.

1.1 lvalue (Locator Value)

Bellekte kalıcı bir konuma (persistent location) sahip bir nesneyi temsil eder.

Örneğin:

Bu nesnelerin bir adı vardır veya kendisine başvurulabilir (ör. x).

Atama işleminin sol tarafında (veya sağ tarafında) yer alabilir.

1.2 rvalue (Right-Hand Value)

Kalıcı bir bellek konumuna sahip olmayan geçici nesneyi veya değeri temsil eder.

Örnek:

Adresi alınamaz. Tam ifade (full expression) bittiğinde yok olurlar (eğer bir değişkende saklanmazlarsa).

1.3 xvalue, glvalue ve prvalue (C++11 ve Sonrası)

Modern C++’ta referanslar daha ince ayrımlarla tanıtılmıştır:

prvalue (Pure rvalue):

Geçici nesneleri veya sabit değerleri (literal) temsil eder.

xvalue (Expiring value):

Kaynaklarının yeniden kullanılabileceği nesneyi temsil eder (örnek: std::move ile elde edilen sonuç).

glvalue (Generalized lvalue):

lvalue ve xvalue’ları birleştirerek bellekte bir konuma sahip nesneleri temsil eder. Kabaca tüm lvalue’lar ve rvalue referanslarını içeren ifadeler glvalue’lardır.

1.4 Referans Tablosu

Özetle, C++’ta değerlerin kategorileri aşağıdaki gibi sınıflandırılabilir:

2. Sahiplik ve Yaşam Zamanı Kavramları

2. Sahiplik ve Yaşam Zamanı Kavramları

Rust’tan aşina olan insanlar vardır, mutuatörler ve sahiplik kavramları C++’ta da mevcuttur. Bu kavramlar, nesnelerin yaşam sürelerini ve kaynak yönetimini anlamak için önemlidir ve modern C++’da sahiplik ve yaşam zamanı (lifetime), kaynak yönetiminin temelini oluşturur. Sahiplik, bir kaynağın kimin kontrolünde olduğunu ifade ederken, yaşam zamanı, o kaynağın bellekte ne kadar süreyle var olacağını tanımlar. Bu iki kavram birbiriyle yakından ilişkilidir çünkü kaynağın bellekte bulunacağı süreyi o kaynağa sahip olan belirler ve modern C++’ın sahiplik modeli, yaşam zamanı sorunlarını çözmek ve hataları önlemek için güçlü araçlar sunar.

2.1 Yaşam Zamanları

Yaşam zamanları nesnenin tanımlanmasına bağlı olarak belirlenir. Nesnenin yaşam zamanınını 4 farklı durumda inceleyebiliriz:

  • Otomatik Yaşam Zamanı (Stack Nesneleri): Yerel değişkenler, ifade blokları içerisinde tanımlanır ve ifade blogundan çıktığında otomatik olarak yok edilir.
  • Dinamik Yaşam Zamanı (Heap Nesneleri): Bellek yönetimi operatörleri (new ve delete) ile oluşturulan nesneler, programcı tarafından bellekten serbest bırakılana kadar bellekte kalır.
  • Statik Yaşam Zamanı (Global ve Statik Nesneler): Global değişkenler ve static anahtar kelimesiyle tanımlanan değişkenler, programın başlangıcından sonuna kadar bellekte kalır.
  • Geçici Yaşam Zamanı (Temporary Objects): Geçici nesneler, ifadelerin değerlerini geçici olarak saklamak için oluşturulur ve ifade tamamlandığında yok olur.

2.2 Sahiplik ve Yaşam Zamanı İlişkisi

Yaşam zamanlarını üç aşağı beş yukarı anladık diye düşünüyorum. Sahiplik de daha önce belirttiğim gibi, bir kaynağın yaşam zamanı boyunca onun yönetiminden kimin sorumlu olduğunu belirler. Eğer sahiplik ve yaşam zamanı doğru yönetilmezse, şu sorunlar ortaya çıkabilir:

  • Çift Serbest Bırakma: Aynı kaynağın birden fazla kez serbest bırakılması.
  • Bellek Sızıntısı: Belleğin serbest bırakılmaması.
  • Geçersiz İşaretçi Kullanımı Yaşam süresi sona eren bir nesneye erişim.

Örneğin:

bu gibi durumlar, programın beklenmedik şekilde davranmasına ve hatalara yol açabilir (en iyimser ihtimalle segmentation fault alınır).

2.3 Modern C++’ta Sahiplik Modelleri: RAII

Modern C++, Resource Acquisition Is Initialization (RAII) prensibiyle sahiplik ve yaşam zamanı sorunlarını çözer. RAII, bir kaynağın sahipliğini bir nesneye bağlayarak, kaynağın yaşam süresini nesnenin yaşam süresiyle eşleştirir.

Bu prensip kısaca şu şekilde çalışır:

  • Her bir kaynağı bir sınıfın içine kapsülleyin, burada:
  • Yapıcı (constructor), kaynağı edinir ve tüm sınıf değişmezlerini (invariant) oluşturur ya da bunu yapamıyorsa bir istisna (exception) fırlatır.
  • Yıkıcı (destructor), kaynağı serbest bırakır ve hiçbir zaman istisna fırlatmaz.
  • Kaynağı her zaman bir RAII sınıfının bir örneği aracılığıyla kullanın, bu sınıfın:
  • Ya kendisi otomatik depolama süresine (automatic storage duration) sahip olması ya da geçici (temporary) bir yaşam süresine sahip olması,
  • Ya da yaşam süresinin, bir otomatik ya da geçici nesnenin yaşam süresiyle sınırlı olması gerekir

3. Akıllı İşaretçiler ile Yaşam Zamanı Yönetimi

Akıllı işaretçiler (smart pointers), RAII prensibini uygulayarak bellek yönetimini kolaylaştırır ve bellek sızıntılarından kaçınmanıza yardımcı olur. Modern C++’ta, std::unique_ptr, std::shared_ptr ve std::weak_ptr gibi standart kütüphane sınıfları, bellek yönetimini otomatikleştirir ve kaynakların güvenli bir şekilde serbest bırakılmasını sağlar.

3.1 std::unique_ptr

yalnızca bir nesneye sahip olabilir

std::unique_ptr, oluşturulan bir nesneye ait her zaman tek bir işaretçiye sahip olabileceğiniz anlamına geliyor, yani o işaretçiye sahip olan başka bir işaretçi olamaz, ve bu nesne yaşam zamanı bittiğinde yıkıcı çağırılarak kaynak otomatik olarak serbest bırakılır. Böylece aynı bellek alanını iki kez silme gibi bir sorun yaşamazsınız.

3.2 std::shared_ptr

std::shared_ptr, oluşturulan bir nesneye ait birden fazla işaretçiye sahip olabilir, her kullanıldığı zaman sayacı artırılır ve nesneyi kullanan işaretçi sayısı 0 olduğunda nesne serbest bırakılır.

Aslında bu örnek pek yerinde olmadı çünkü std::shared_ptr‘nin kullanımı daha çok bir nesneye birden fazla işaretçiye sahip olmak istediğiniz durumlarda kullanılır ve bunu en güzel açıklayacağımız örnek threadlerdeki paylaşılan verilerin güvenli bir şekilde paylaşılmasında kullanılabilir. Onu da örneklendireceğim :) geliyor.

3.3 std::weak_ptr

std::weak_ptr, std::shared_ptr‘nin zayıf bir referansıdır ve bir nesneye sahip olmaz, yalnızca bir std::shared_ptr‘den oluşturulabilir ve bu sayede döngüsel referansları önler. Şimdi bu bu konuyu örneklendireceğim ama hem thread’lerde buna örnek vermek istiyorum hem de bu referans çeşitini ben bile tam olarak anlamadım :).

4. Move Semantiği ve Sahiplik Transferi

4.1. Neden Move Semantiği Var?

Dimi, sahiplik transferine gerçekten ihtiyacımız var mı? Evet, var. Klasik C++’ta (C++11 öncesi), nesneler varsayılan olarak kopyalanırdı. Bunu şöyle düşünün, elinizde bir kaynak var, onu başka bir fonksiyona göndermek istiyorsunuz, bu durumda hem elinizdeki kaynak, hem de fonksiyona gönderdiğiniz kaynak aynı anda bellekte olacak ve bu durumda iki kaynak için de bellekten ayrılan alanlar olacak ve bu durumda iki kaynak için de ayrı ayrı bellek alanları serbest bırakılması gerekecek. Bu durum bellek sızıntılarının temel nedenlerinden birisidir. Ayrıca kopyalama maliyeti yüksek olan bir iştir, hele ki std::vector, std::string gibi kaynak yöneten nesneler için derin kopyalama (deep copy) masrafı nedeniyle pahalıdır. Move semantics (taşıma semantiği), kaynakları kopyalamak yerine taşımaya olanak tanıyarak performansı optimize eder. Bu, bellek veya dosya kolları (file handles) gibi dinamik kaynaklar yöneten nesneler için özellikle faydalıdır.

4.2. Move Semantiği Nasıl Çalışır?

  • Move Constructor: Kaynakları bir nesneden diğerine aktarır, kaynağı geçerli ancak tanımsız bir duruma bırakır.
  • Move Assignment Operator: Halihazırda başlatılmış nesneler için, move constructor’a benzer şekilde kaynak aktarımı yapar.

Örnek: Move Constructor Uygulaması

Temel Noktalar:

  • std::move, bir lvalue’ı xvalue’a dönüştürerek move kurucusunun devreye girmesini sağlar.
  • Taşıma işleminden sonra, kaynak nesne (vec1) boşalır fakat geçerli (valid) kalır.

Örnek: Move Atama Operatörü Uygulaması

4.3. Değerler ve Move Semantiği Arasındaki Bağlantı

Move Semantiğinde rvalue’ların Rolü Move semantiği, rvalue’ları performansı optimize etmek için kullanır:

  • rvalue’lar geçicidir ve “taşınmaları” güvenlidir.
  • Fonksiyonlar ve kurucular, lvalue referansları (T&) ve rvalue referansları (T&&) için sıklıkla aşırı yüklenir (overload).

Örnek: lvalue ve rvalue Referansları için Aşırı Yükleme

Çıktı:

Kopya kurucu çağırıldı
Move kurucu çağırıldı

4.4. Move Semantiği ve Bellek Yönetimi

RAII’yi sağlamak için bazı temel noktalar var:

  • Orjinal nesneye artık ihtiyacınız olmadığında ve sahipliği devretmenin güvenli olduğu durumlarda std::move kullanın.
  • Gereksiz kullanımlardan kaçının; aksi takdirde nesneleri beklenmedik bir şekilde geçersiz (invalidate) duruma sokabilirsiniz.
  • Büyük nesnelerde, pahalı derin kopyaları önlemek için taşıma (move) tercih edin.
  • Dosya kolları veya bellek tamponları gibi dinamik kaynakları yöneten sınıflarda move semantiği kullanın.

Move Semantiği ile Etkin Fonksiyon Dönüşü

Büyük nesnelerin döndürülmesinde move semantiği büyük avantaj sağlar:

Modern derleyicilerde, return value optimization (RVO) veya move işlemleri kaynak yönetimini verimli kılar.

Dipnot: nullptr ve std::nullptr_t

Şu diğer dillerde bulunan Null Safety kavramı geçmiş 2 senemi (Dart ve Kotlin sayesinde) işgal ettiği için nullptr değinmeden geçemeyeceğim.

nullptr, C++11’de tanıtılan bir özel değerdir ve bir işaretçinin (pointer) geçerli olmadığını belirtir. Tarihin eski çağlarından kalan NULL ile karşılaştırıldığında nullptr, işaretçi türlerinde daha güvenli ve tutarlı bir şekilde kullanılır. Örneğin NULL aynı zamanda 0 gibi algılandığı için yani bir çeşit işlev aşırı yüklemesine sahip olduğu için NULL istenmeyen dönüşümlerde kafa karışıklığına yol açabilir. nullptr ise dilin içerisinde açık bir şekilde null pointer’ı temsil eder ve bu tür sorunları önler.

nullptr öncesinde söylediğim gibi bir değerdir, ve std::nullptr_t türündedir. Herhangi bir işaretçi türüne örtük olarak dönüştürülebilir, ancak herhangi bir tamsayı türüne dönüştürülemez. Bu durum, işaretçi ve tamsayılar arasında yapılan aşırı yükleme çağrılarında oluşabilecek belirsizlikleri ortadan kaldırır.

Lambda İfadeleri

Lambdalar Nedir?

Lambda expressions, satır içi (inline) tanımlanabilen anonim fonksiyonlardır. Özellikle sıralama (sorting), filtreleme (filtering) ve geri çağrı (callback) gibi kısa süreli işlemler için idealdir. Lambdalar, fonksiyonel programlamayı sadeleştirir ve STL algoritmaları ile sorunsuz bütünleşir.

Neden Lambdalar Kullanılır?

  • Kısa ve Özdür: Ekstra fonksiyon nesneleri veya şablon (boilerplate) kod yazma ihtiyacını ortadan kaldırır.
  • Esnektir: Değer veya referans olarak değişken yakalamayı (capture) destekler, böylece lambda içinde bu değişkenleri kullanabilirsiniz.

Sözdizimi

[ capture ] ( parameters ) -> return_type {
// Fonksiyon gövdesi
}

Örnekler

Basit Bir Lambda:

Lambda ve STL Algoritmaları Kullanımı:

Değişken Yakalama (Capture):

Concurrency (Eşzamanlılık) — std::thread ve Diğerleri

Concurrency Nedir?

Eşzamanlılık (concurrency), programın aynı anda birden fazla görevi yürütmesine imkân tanıyarak performans ve etkileşimi (responsiveness) artırır. C++11 öncesinde eşzamanlılık çoğunlukla platforma özgü kütüphanelere bağlıydı ve taşınabilirlik (portability) sorunları yaşanıyordu.

1. C++11’de Eşzamanlılık Desteği

C++11, eşzamanlılık desteği için standart kütüphaneler ekleyerek bu sorunu çözer. Bu kütüphaneler, iş parçacıkları (threads), senkronizasyon (mutex) ve gelecekteki sonuçlar (futures) gibi modern eşzamanlılık araçlarını içerir.

  1. std::thread: Thread (iş parçacığı) oluşturma ve yönetme.
  2. std::mutex: Paylaşılan kaynaklara eşzamanlı, güvenli erişim sağlama.
  3. std::future ve std::promise: Asenkron programlamada sonuç senkronizasyonu sağlar.

Şimdi hangisi ne için kullanılacak diye sorduğunuzu duyar gibiyim.Kural oldukça basit:

  • Hafif eşzamanlılık gerektiren görevler için std::thread kullanın.
  • Paylaşılan kaynakları std::mutex ile koruyarak veri yarışlarını (data race) önleyin.
  • Sonuç senkronizasyonuna ihtiyaç duyan görevler için std::future’ı tercih edin.

2. Eşzamanlılık Sağlayan C++11 Özellikleri

Burada bazı örnekler vererek devam edeceğim ama bu konuların detaylarına ilerleyen yazılarımda mutlaka gireceğim.

Thread Oluşturma ve Birleştirme (join):

std::mutex ile Thread-Güvenli Erişim:

std::future ile Sonuç Senkronizasyonu:

Concurrency ve Bellek Yönetimi

Eşzamanlılık ve bellek yönetimi, programın performansını ve güvenilirliğini etkileyen kritik bir konudur. Eşzamanlı işleri yürütürken, paylaşılan kaynaklara eşzamanlı erişimde güvenlik sorunları ortaya çıkabilir. Bu sorunları önlemek için, std::mutex ve std::lock_guard gibi eşzamanlılık araçlarını kullanarak Modern C++’da verilerin güvenliğini sağlanabilir.

Şimdi önceki kısımlarda örneklerini gördüğümüz yapıları tek bir örnekte, bellek yönetimi amacıyla nasıl kullanabiliriz, onu ele alalım:

Bu örnek, kaynak yönetimi için eşzamanlılık kalıplarını akıllı işaretçilerle birleştirerek modern C++’da çok iş parçacıklı programlama (multithreading) için en iyi uygulamaları gösterir. Bu örnekte, Logger ve TaskQueue sınıfları, paylaşılan kaynaklara güvenli erişim sağlamak için std::mutex kullanır. TaskQueue, iş parçacıklarının paylaşılan görevleri almasını ve yürütmesini sağlar. Logger, iş parçacıklarının durumunu izlemek ve günlüğe kaydetmek için kullanılır. stop() yöntemi, hiçbir görev kalmadığında iş parçacıklarının temiz bir şekilde çıkmasını sağlar.

Atomic ve Lock-Free Programlama

Bu yazı oldukça uzadı ama bu konuyu da atlamak istemedim. std::atomic ve lock-free programlama, modern C++’ın eşzamanlılık ve performansı artırmak için sunduğu güçlü araçlardır. std::atomic, halihazırda mutex gibi yapılarla korunmayan veri türleri ile oluşturulmuş ve birden fazla iş parçacığı ile paylaşılan değişkenlere eşzamanlı erişim sağlar. ancak sadece korunmayan veri türleri veya temel türler ile çalışır gibi bir yanılgıya düşmeyin, kullanıcı tanımlı türler için de (kendi oluşturduğunuz mutex içeren veri türleri için ne kadar mantıklıdır bilmem ama) kullanılabilir.

Atomik işlemler, başka bir işlem tarafından kesintiye uğramadan tamamlanır, yani birden fazla iş parçacığı aynı değişkene aynı anda erişmeye çalışsa bile verileri bellek erişimi esasına göre güvenli bir şekilde günceller. Bu türün kullanımı, standart kütüphanede std::shared_ptr ile ortaya çıkmış olup, mutex ve diğer senkronizasyon araçlarına kıyasla daha hızlı bir arayüz sunar.

Bitirirken…

Sözün özü Move semantics, lambda expressions ve concurrency gibi özelliklerle C++, geliştiricilere verimli ve modern kod yazma konusunda güçlü araçlar sunar. Bu özellikler yalnızca performansı artırmakla kalmaz, aynı zamanda karmaşık programlama görevlerini de basitleştirir.

Bir ileri seviye Modern C++ özelliklerine daha derinlemesine bakacağız.

--

--

No responses yet