C++ProgramlamaKıdemli C++ Geliştirici

**std::unique_ptr**'ın özel deleter desteğini **std::shared_ptr**'ınkinden ayıran nedir, tür silme ve nesne boyutu etkileri açısından?

Hintsage yapay zeka asistanı ile mülakatları geçin

Sorunun cevabı

C++11 ile birlikte std::unique_ptr ve std::shared_ptr güvenli olmayan std::auto_ptr'ın yerine geçmek üzere tanıtıldı. Her ikisi de dosya tanıtıcıları veya veritabanı bağlantıları gibi bellek dışı kaynakları yönetmek için özel deleter'ları destekler. Ancak, sahiplik modelleri ve performans gereksinimleri nedeniyle mimari yaklaşımları köklü bir şekilde farklıdır.

std::unique_ptr, münhasır sahipliği uygular ve deleter'ını türün bir parçası olarak depolar (ikinci şablon parametresi). Eğer deleter durumsal ise, yönetilen işaretçi ile birlikte unique_ptr nesnesinin içinde yer kaplar. std::shared_ptr ise paylaşılan sahiplik uygulayarak, deleter’ın türünün silindiği ve shared_ptr nesnesinden ayrı olarak depolandığı heap üzerinde bir kontrol bloğu tahsis eder.

Bu mimari fark, farklı boyut özelliklerine neden olur. Durumsal olmayan bir deleter'a sahip bir std::unique_ptr, Empty Base Optimization sayesinde tam olarak bir ham işaretçinin (raw pointer) boyutunu kaplar. Buna karşılık, std::shared_ptr, deleter'ın boyutundan veya karmaşıklığından bağımsız olarak sabit bir boyut (genellikle iki işaretçi) sürdürür, çünkü deleter ayrı olarak tahsis edilen kontrol bloğunda yer alır.

#include <memory> #include <cstdio> #include <iostream> struct FileDeleter { void operator()(FILE* fp) const { if (fp) std::fclose(fp); } }; struct StatefulDeleter { int flags = 0xDEAD; void operator()(FILE* fp) const { if (fp) std::fclose(fp); } }; int main() { // stateless deleter ile unique_ptr: boyut == işaretçi boyutu (64 bit'te 8 bayt) std::unique_ptr<FILE, FileDeleter> up(nullptr); // shared_ptr: deleter'a bağlı olarak sabit boyut (16 bayt) std::shared_ptr<FILE> sp(nullptr, FileDeleter{}); std::cout << "Unique (stateless): " << sizeof(up) << " bayt "; std::cout << "Shared (herhangi bir deleter): " << sizeof(sp) << " bayt "; // durumsal deleter ile unique_ptr: daha büyük boyut (16 bayt: işaretçi + int + padding) std::unique_ptr<FILE, StatefulDeleter> up2(nullptr, StatefulDeleter{}); std::shared_ptr<FILE> sp2(nullptr, StatefulDeleter{}); std::cout << "Unique (stateful): " << sizeof(up2) << " bayt "; std::cout << "Shared (stateful): " << sizeof(sp2) << " bayt "; }

Gerçek hayattan bir durum

Bir geliştirme ekibi, bir C API'si tarafından döndürülen eski veritabanı bağlantı yöneticilerini (void*) yönetmek zorundaydı. Bu yöneticilerin, delete yerine db_disconnect() ile özel bir temizleme gerektiriyordu. Uygulama, saniyede binlerce yöneticinin yaratıldığı sık döngülerde çalışıyordu, bu da bellek ayak izi ve tahsis performansını kritik hale getiriyordu.

İlk düşünülen yaklaşım, yöneticiyi depolayan ve db_disconnect()'i yok edicisinde çağıran bir özel RAII sarmalayıcı sınıfı ConnectionGuard oluşturmaktı. Artıları, arayüz üzerinde tam kontrol ve bağlantıya özel yöntemler ekleyebilme yeteneğiydi. Eksileri, her kaynak türü için önemli ölçüde boilerplate kod, işaretçi semantiğinin yeniden icadı ve akıllı işaretçiler için tasarlanmış standart kütüphane algoritmaları ile uyumsuzluktu.

İkinci çözüm, std::shared_ptr<void>'ı bir lambda deleter ile kullanarak db_disconnect() fonksiyonunu yakaladı. Artıları, standart bileşenler kullanarak anında erişilebilirlik ve gerektiğinde sahipliği paylaşma imkânıydı. Eksileri, kontrol bloğu için zorunlu heap tahsisi, yüksek frekansta benzersiz sahiplik için uygun olmayan atomik referans sayım yükü ve ayrıca bağlantı yöneticisinin hafif doğasıyla bağımsız olarak sabit bir nesne boyutunun olmasıydı.

Üçüncü yaklaşım, std::unique_ptr<void, decltype(&db_disconnect)> ile bir fonksiyon işaretçisi deleteri veya tercihen durumsal olmayan bir functor kullanmasını içeriyordu. Artılar, durumsal olmayan functor'lar kullanıldığında Empty Base Optimization sayesinde sıfır yük, heap tahsisi olmaması ve münhasır sahiplik semantiklerini mükemmel bir şekilde ifade etme imkanına sahip olmasıydı. Eksileri, tür imzasının ayrıntılı olması ve çalışma zamanında deleter'ları değiştirememekti.

Ekip, durumsal olmayan bir functor deleteri ile üçüncü çözümü seçti. Bu seçim, heap tahsislerini tamamen ortadan kaldırdı, sarmalayıcı boyutunu 8 bayta düşürdü ve otomatik temizleme sağlarken atomik işlem yükünü kaldırdı.

Sonuç olarak, bellek kullanımında %40'lık bir azalma ve bağlantı gruplama sisteminde önemli gecikme iyileştirmeleri elde edildi, istisna güvenliği sağlanırken performanstan ödün verilmedi.

Adayların sıklıkla kaçırdığı noktalar


Neden std::unique_ptr, varsayılan deleteri kullanırken yok edim noktası itibariyle eksiksiz bir tür gerektirirken, std::shared_ptr bunu gerektirmiyor?

Cevap: std::unique_ptr, varsayılan deleteri ile yönetilen işaretçi üzerinde delete çağırır. C++ standardı, T'ye işaret eden bir işaretçi üzerinde delete çağırmanın, yok ediciyi çağırmak ve boşaltım için boyutu hesaplamak üzere T'yi tamamlanmış bir tür olarak tanımladığını gerektirir. Eğer unique_ptr'ın yok edicisi, T yalnızca önceden bildirilmiş yerde oluşturulursa, derleme hatası oluşur. std::shared_ptr, yok ediciyi (T'yi nasıl yok edeceğini bilen) inşa etme zamanında kontrol bloğuna yakalar. Yok edici tür silindiği ve ayrı depolandığı için, shared_ptr daha sonra T'nin eksik olduğu yerlerde yok edilebilir. Bu ayrım, Pimpl (Implementation'a Göstergesi) tarifinin önemli bir parçasıdır: shared_ptr, kaynak dosyalarında uygulama ayrıntılarını gizlemeye izin verirken unique_ptr, ya tam türlere ya da ayrımın görüldüğü yerde tanımlanan açık özel deleter'ları gerektirir.


Neden std::make_unique özel deleter'ları desteklemiyor ve önerilen alternatif nedir?

Cevap: std::make_unique (C++14'te tanıtıldı) istisna güvenli tahsis sağlayarak yalnızca std::unique_ptr<T> veya std::unique_ptr<T[]> döndürür; bunlar std::default_delete'i kullanır. Fonksiyon, deleter türünü argümanlardan çıkaramadığı için deleter türü unique_ptr şablon imzasının bir parçası olmalıdır ve fabrika fonksiyonları açık şablon parametreleri olmadan özel deleter türlerini dolaylı olarak çıkaramaz. Önerilen alternatif, doğrudan yapılandırmadır: std::unique_ptr<T, CustomDeleter>(new T(args), CustomDeleter{...}). Bu yaklaşım, deleter türünü şablonda açıkça belirlerken özel kaynak temizleme mantığına izin verir, ancak istisna güvenliği garantilerini korumak için manuel istisna işleme veya dikkatli yapılandırma sırası gerektirir.


Empty Base Optimization, stateless deleter'lar kullanıldığında std::unique_ptr bellek düzenini nasıl etkiler ve bu neden std::shared_ptr için mevcut değildir?

Cevap: std::unique_ptr, deleter bir sınıf türü olduğunda deleter sınıfından miras alır. Eğer deleter veri üyesi içermezse (durumsal değilse), C++ Empty Base Optimization (EBO)'yu uygular ve boş temel alt nesne sıfır bayt kaplamasına izin verir. Sonuç olarak, sizeof(std::unique_ptr<T, StatelessDeleter>) sizeof(T*) ile eşit olur ve sıfır yük ile soyutlama sağlanır. std::shared_ptr, tür silmesini desteklemek zorunda olduğu için EBO'yu kullanamaz: aynı T'ye ait herhangi bir shared_ptr deleter'dan bağımsız olarak aynı boyutta olmak zorundadır. Bu nedenle, shared_ptr deleter'ı shared_ptr nesnesinin dışında, heap-tahsisli kontrol bloğunda depolar. Bu tasarım, deleter'ların çalışma zamanı çok biçimliliğini sağlar, ancak heap tahsisini zorunlu kılar ve unique_ptr'ın faydalandığı stack-alan optimizasyonunu engeller.