Tarihçe: C++11'den önce, std::vector yeniden tahsis sırasında yalnızca kopyalama işlemlerine dayanıyordu çünkü taşıma semantiklerinin olmadığı bir dönemdi. C++11'de taşınabilirlik semantiklerinin tanıtılması, önemli performans iyileştirmeleri sözü verdi, ancak kritik bir güvenlik ikilemi getirdi: Eğer bir taşıma yapıcısı yeniden tahsis sırasında bir istisna fırlatırsa, konteyner kolayca geri dönemez çünkü kaynak nesneler taşınmış durumunda bırakılabilir.
Problem: std::vector kapasitesi tükendiğinde ve büyümesi gerektiğinde, mevcut öğeleri yeni bir belleğe transfer etmesi gerekir. Bu işlem sırasında bir istisna meydana gelirse, güçlü istisna güvenliği garantisi konteynerin orijinal durumunda kalmasını (her şey ya da hiçbiri semantiği) şart koşar. Ancak, taşıma yapıcıları istisna fırlatırken bu güvenliği ihlal eder çünkü kaynak nesneleri yıkıcı bir şekilde değiştirir; eğer 100. taşıma sırasında bir istisna fırlatılırsa, önceki 99 öğe zaten yok olur ya da geçersiz hale gelir, bu da geri dönmeyi imkansız hale getirir.
Çözüm: C++ standartları, std::vector'ün std::move_if_noexcept (veya std::is_nothrow_move_constructible aracılığıyla eşdeğer derleme zamanı özellik tespiti) kullanarak hareket ve kopyalama işlemleri arasında seçim yapmasını şart koşar. Eğer öğe türünün taşıma yapıcısı noexcept olarak işaretlenmemişse, vektör tedbirli bir şekilde kopyalama işlemlerine geri döner. Çünkü kopyalar kaynak nesneleri sağlam bırakır, bir istisna yakalanabilir ve orijinal bellek alanı dokunulmadan kalır, güçlü garantiyi korur.
struct Data { std::vector<int> payload; // Tehlikeli: vektörün taahhüt edilmediği için dolaylı olarak noexcept(false) Data(Data&& other) noexcept(false) : payload(std::move(other.payload)) {} Data(const Data&) = default; }; std::vector<Data> v; v.reserve(2); v.push_back(Data{}); v.push_back(Data{}); // Büyüme gerektiren bir sonraki push_back durumunda: // Eğer Data'nın taşıması noexcept değilse, vektör tüm öğeleri kopyalar
Problem Tanımı: Yüksek frekanslı bir ticaret motorunda, canlı piyasa derinliğini temsil eden bir std::vector sipariş defteri anlık görüntüleri tutuyorduk. Piyasa açılma artışları sırasında vektör sık sık büyüme gerektiriyordu. Sistem, ultra düşük gecikme (mikrosaniye hassasiyeti) ve kesin çökme güvenliği gerektiriyordu—yeniden tahsis sırasında herhangi bir istisna sipariş defteri durumunu bozmak veya bellek sızıntısına neden olamazdı.
Çözüm 1: Aşırı tahsis ile ön rezervasyon Tamamen yeniden tahsis gerektirmemek için büyük bir ön kapasite (örneğin, 1 milyon öğe) tahsis etmeyi düşündük. Artılar: Büyüme sırasında istisna riskini ortadan kaldırır, işaretçi istikrarını garanti eder. Eksiler: Düşük aktivite dönemlerinde (günün %99'u) önemli RAM israfı, birlikte yerleştirilen sunucuların bellek kısıtlarını ihlal eder ve kapasiteyi aşan kara kuğu olaylarını ele almaz.
Çözüm 2: std::list'e geçiş Yeniden tahsis ihtiyaçlarını ortadan kaldırmak için vektörü std::list ile değiştirmek. Artılar: Doğal olarak güçlü istisna güvenliği garanti edilir, kararlı yineleyiciler. Eksiler: Önbellek yerelliği yok olur (5-10 kat daha yavaş yineleme), düğüm başına bellek aşımı (ekstra 16-24 byte), çok iş parçacıklı ortamda ayırıcı çatışmalara neden olan parçalama.
Çözüm 3: noexcept taşıma işlemleri zorlanması Tüm anlık görüntü türlerini yığın kaynaklar için std::unique_ptr kullanacak şekilde yeniden yapılandırmak ve taşıma yapıcılarını açıkça noexcept olarak işaretlemek. Artılar: Hızlı taşımaları (kopyalamaktan %80 daha hızlı) sağlar, güçlü istisna güvenliğini korur, standart konteynerlerle uyumludur. Eksiler: Taşıyan yollardaki istisna fırlatma işlemlerinin olmaması için titiz bir kod incelemesi gerektirir, sınıf tasarımında kısıtlamalar (taşımalar sırasında istisna fırlatan kaynak edinimi kullanamaz).
Seçilen Çözüm: Çözüm 3'ü seçtik ve tüm kritik veri yapılarını noexcept-taşınabilir hale getirmek için bir kod tabanı denetimi gerçekleştirdik. Regresyonları önlemek için static_assert(std::is_nothrow_move_constructible_v<Data>) kullanarak statik doğrulamalar ekledik.
Sonuç: Piyasa artışları sırasında gecikme %42 azaldı ve stres testleri sırasında enjekte edilen istisnalarda sıfır bozulma olayı yaşandı. Sistem, istisna güvenliği için düzenleyici denetim gerekliliklerini geçmiştir.
Neden std::vector özellikle yeniden tahsis sırasında güçlü istisna güvenliği gerektirir, temel garantiden ziyade?
Temel istisna güvenliği yalnızca programın geçerli bir durumda kalmasını ve kaynak sızıntısı olmamasını gerektirir; bu, konteynerin kısmen taşınmış bir durumda bırakılmasına izin verir. Ancak, yeniden tahsis kullanıcı perspektifinden atomik bir işlemdir—bellek işaretçisi değişir ya da değişmez. Eğer std::vector yalnızca temel güvenliği sağlarsa, bir istisna konteyneri bazı öğeleri eski bellekte, bazılarını ise yeni bellekte bırakarak, ya da tutarsız bir boyut/kapasite sayısıyla terk edebilir, bu da sınıf invariyantlarını ihlal eder ve sonraki işlemlerde tanımsız davranışa neden olur. Güçlü garanti işlemelere ait bir anlam sağlamaktadır: ya büyüme tamamen başarılı olur ya da vektör tam olarak olduğu gibi kalır.
Derleyici, noexcept taşıma yapıcıları için denetimi zamanlama aşamasında nasıl optimize eder?
std::vector, std::is_nothrow_move_constructible<T>'i kullanır, bu bir derleme zamanı özelliğidir. Uygulama genellikle, eğer taşıma yapıcısı fırlatılabilecekse kopyayı tetikleyen bir lvalue referansı dönen, aksi halde taşıma tetikleyen bir rvalue referansı dönen bir işlev şablonu olan std::move_if_noexcept kullanır. Bu yönlendirme, fonksiyon aşırı yüklemesi ve şablon oluşturması yoluyla derleme sırasında gerçekleşir ve zamanlama aşamasında ek yük getirmeden optimal kod yolları oluşturur. Eğer taşıma noexcept olarak kanıtlanmışsa, derleyici tamamen geri dönüş kopya yolunu ortadan kaldırabilir ve sıfır maliyetli bir soyutlama sağlar.
Eğer bir tür yalnızca taşınabilir (kopyalanamaz) ve taşıma yapıcısı noexcept değilse ne olur?
Eğer std::unique_ptr gibi (taşınabilir yalnızca) bir tür, bir istisna fırlatabilen bir taşıma yapıcısına (varsayımsal) sahip olursa, std::vector imkansız bir seçimle karşılaşır: kopyalayamaz (tür kopyalanamaz) ve güvenli bir şekilde taşıyamaz (istinaf fırlatabilir). C++17'den önce, bu durum yeniden tahsis gerektiren işlemler için derleme hatalarına yol açıyordu. Ancak C++17 ile, standart std::vector'ün yine de fırlatma yapan taşıyıcıyı kullanacağını ancak yalnızca temel istisna güvenliği sağlayacağını şart koşar—eğer taşıma istisna fırlatırsa, öğeler kaybolabilir veya konteyner belirtilen bir geçerli durumun içinde kalabilir. Bu nedenle, standart kütüphanedeki tüm yalnızca taşıma türleri (std::unique_ptr, std::fstream gibi) noexcept taşımalara garanti verir ve özel taşıma türlerinin de bunu takip etmesi gerektiği anlamına gelir.