C++ProgramlamaC++ Geliştirici

Neden sınıf içindeki bir yıkıcının varsayılan olması, yıkıcının kendisi basit olsa bile, örtük taşınma işlemlerini engeller?

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

Sorunun cevabı

Geçmiş: C++98'de, kaynak yönetimi Üç Kuralı takip ediyordu: bir sınıf özel bir yıkıcı, kopya oluşturucu veya kopya atama operatörüne ihtiyaç duyuyorsa, muhtemelen hepsine ihtiyaç duyuyordu. C++11 taşınma semantiklerini tanıttığında, bu Beş Kuralı haline geldi ve taşınma oluşturucusu ve taşınma atama operatörü eklendi. Standart komitesi temkinli bir yaklaşım benimsedi: herhangi bir yıkıcının (hatta basit olanların bile) beyan edilmesi, yıkıcılar tarafından yönetilen kaynakların kazara yüzeysel taşınmasının önlenmesi için taşınma işlemlerinin örtük olarak oluşturulmasını engeller.

Sorun: ~MyClass() = default; yazdığınızda, "kullanıcı tarafından beyan edilen" bir yıkıcı oluşturursunuz. C++ standardına ([class.copy.ctor]/3) göre, bu durum taşınma oluşturucusunun ve taşınma atama operatörünün örtük olarak beyan edilmesini engeller. Sonuç olarak, derleyici sınıfı yalnızca kopya olarak değerlendirir ve std::vector yeniden tahsislerinde veya değerle döndürmeye yönelik optimizasyonlarda sessizce pahalı kopyalama semantiğine geri döner, yıkıcı aslında hiçbir iş yapmasa bile.

Çözüm: Örtük taşınma oluşturulmasını sürdürmek için, yıkıcıyı yalnızca sınıf içinde beyan edin ve varsayılan tanımı dışarıda sağlayın:

class Optimized { public: ~Optimized(); // Yalnızca burada beyan edildi std::array<char, 4096> buffer; }; Optimized::~Optimized() = default; // Dışarıda tanımlandı

Bu, yıkıcıyı "kullanıcı tarafından sağlanmış" ama derleyicinin taşınma oluşturacağı noktada "kullanıcı tarafından beyan edilmemiş" hale getirir. Alternatif olarak beş özel üyesi de açıkça varsayılan hale getirin veya tercihen, ham kaynakları std::unique_ptr veya konteynerlerle değiştirerek Sıfır Kuralını takip edin.

Hayattan bir durum

Bir yüksek frekanslı ticaret motorunda MarketDataPacket nesnelerini işlerken bu durumla karşılaştık. Sınıf, ağ verileri için sabit 4KB'lık bir tampon tutuyordu:

class MarketDataPacket { public: ~MarketDataPacket() = default; // "Açıklık" için başlıkta yazıldı char buffer[4096]; };

C++11'e geçtikten sonra, gecikme profilleme, değerle paket döndürülmesine rağmen CPU döngülerinin %40'ının memcpy içinde harcandığını ortaya çıkardı. Suçlu, sınıf içindeki varsayılan yıkıcıydı; bu istenmeden örtük taşınmaları silmiş ve std::vector büyümesi ve işlev dönüşleri sırasında kopyaları zorlamıştı.

Çözüm 1: Açıkça noexcept taşınma oluşturucusu ve atamasını beyan edin. Bu, taşımaları etkinleştirerek performans sorununu derhal çözer. Ancak, bu fonksiyonları eklerken manuel olarak sürdürmeyi gerektirir, ham işaretçiler söz konusu olduğunda istisna spesifikasyonu uyumsuzlukları riski taşır ve Sıfır Kuralını ihlal eden ek bürokrasi ekler.

Çözüm 2: Yıkıcının tanımını .cpp dosyasına taşıyın ve MarketDataPacket::~MarketDataPacket() = default; yazın. Bu, yıkıcıyı basit tutarak derleyici tarafından üretilen taşınmaları geri getirir. Sıfır-aşırı soyutlamayı korur ve kullanılmayan nesneler için yıkıcı çağrılarının atlanması gibi derleyici optimizasyonlarına olanak tanır. Tek dezavantajı ayrı bir derleme birimi gerektirmesidir ki bu kabul edilebilir bir durumdu.

Çözüm 3: Ham tamponu std::vector<uint8_t> veya std::unique_ptrstd::byte[] ile değiştirin. Bu mükemmel Sıfır Kuralı uyumluluğu sağlar. Ancak, bu, mikrosaniye hassasiyetine sahip ticaret yollarında önbellek yerelliğinin kritik olduğu durumlarda kabul edilemez dolaylılık veya yığın tahsis yükü getirir.

Çözüm 2'yi seçtik. Varsayılan beyanı sınıfın dışına taşıyarak örtük taşımaları geri getirdik, paket işleme gecikmesini 12μs'den 3μs'ye düşürdük ve saldırgan derleyici optimizasyonlarına izin veren basit yıkıcılığı koruduk.

Adayların sık sık kaçırdığı şeyler

Şu durumda compiler, sınıf içindeki ve dışındaki varsayılanlaştırmalar arasında neden ayrım yapar? Semantik olarak aynı olmalarına rağmen.

Fark, sözdizimsel olup, anlamsal değildir. C++, sınıf tanımları için tek geçişli bir analiz modeli kullanır. Derleyici, sınıfın kapanış süresine geldiğinde, örtük taşınma işlemlerini oluşturmaya karar vermelidir. İçeride = default gördüğünde, o noktada yıkıcı "kullanıcı tarafından beyan edilmiş" olur ve [class.copy]/7'ye göre engelleme kurallarını tetikler. Derleyici, bu kararı değiştirmek için dış tanıma "ileri bakamaz". Bu, C++'ın derleme modelinin temel bir kısıtlamasıdır.

Yıkıcıyı noexcept olarak işaretlemek, örtük taşımaları geri getirir mi?

Hayır. Örtük taşınma oluşturulmasının engellenmesi tamamen yıkıcının kullanıcı tarafından beyan edilip edilmemesine bağlıdır, istisna spesifikasyonuna bağlı değildir. Taşımaların std::vector yeniden tahsislerinde kullanılabilmesi için taşımaların noexcept olarak işaretlenmesi çok önemlidir, ancak sınıf içinde varsayılan bir yıkıcıya sadece noexcept eklemek, silinmiş taşınma işlemlerini geri getirmez. Tanımınızı dışarıya taşımak veya taşımaları açıkça varsayılan hale getirmek zorundasınız.

Kullanıcı tarafından beyan edilmiş bir yıkıcı toplu başlatmayı nasıl etkiler?

Herhangi bir kullanıcı tarafından beyan edilmiş yıkıcıya sahip bir sınıf, herhangi bir toplu niteliğe sahip olmaktan çıkar. Bu, genellikle taşımaların kaybından daha yıkıcıdır. Atanmış başlatıcılardan (C++20) ve belirli oluşturucular olmadan süslü parantezle kapsanan başlatıcı listelerini kullanma yeteneğinden mahrum kalmak demektir. Çoğu geliştirici toplu başlatmanın çalışmasını bekler ve çalışmadığında şaşırır:

struct Config { ~Config() = default; // Aggregation'ı kırar int value; }; // Config c{42}; // Hata: eşleşen oluşturucu yok

Bu, kullanıcı tarafından beyan edilen bir yıkıcının varlığının sınıfın tip sistemindeki karmaşık yıkım semanticlerine sahip olmasını zorunlu kılması nedeniyle olur ve bu da aslında karmaşıklıktan bağımsız olarak toplu durumdan mahrum bırakır.