C++17'de standart, garanti edilen kopyalama elision (zorunlu kopyalama elision) tanıtıldı ve bu, prvalue (saf rvalue) nesnelerin oluşturulma şekline köklü değişiklikler getirdi. Bir sınıf türünün prvalue'si, aynı türden bir nesneyi başlatırken—örneğin, bir işlevden değerle dönerken veya geçici bir nesneyi bir işleve geçirirken—nesne doğrudan hedef bellekte inşa edilir. Sonuç olarak, kopyalama konstrüktörü veya hareket konstrüktörü çağrılmaz ve önemli olan, bunların erişilebilirliği (açık vs. özel) veya varlığı (sınıfın tam ve yok edilebilir olması koşuluyla) işlemin geçerli olması için gerekli değildir. Bu, elision'ın yalnızca bir isteğe bağlı optimizasyon olduğu ve derleme için erişilebilir ve mevcut konstrüktörlerin gerekliliğinin olduğu daha önceki standartlarla keskin bir tezat oluşturur.
struct Immovable { Immovable() = default; Immovable(const Immovable&) = delete; Immovable(Immovable&&) = delete; }; Immovable factory() { return Immovable{}; // C++17'de hiç bir hareket/kopyalama çağrılmaz } void consume(Immovable x); // Parametre doğrudan prvalue'dan başlatılır
Ekibimiz, donanım bağlamlarını saran kaynak tutucularının bellek içinde kopyalanamayacağı veya taşınamayacağı bir kernel-mode sürücüsü geliştiriyordu. Bu tutucuların değerle üretilmesi için bir fabrika işlevine ihtiyaç duyduk; ancak, bu tutucular kopyalama ve hareket konstrüktörlerini açıkça silerek, kernel eşleştirmelerinin yanlışlıkla geçersiz hale gelmesini önleme amacı taşıyordu. C++17'den önce, bu tasarım, değerle döndürme ile uyumsuzdu çünkü NRVO olsa bile, derleyici konsept olarak hareket konstrüktörünün erişilebilir olmasını gerektiriyordu ve bu durumda derleme hatalarıyla sonuçlanıyordu.
Çözüm 1: std::unique_ptr ile Yığın Bellek Tahsisi
Tutucuyu std::unique_ptr içinde sarmayı düşündük; bu, göstericinin taşınmasına izin verirken, temel nesnenin sabit kalmasını sağlıyor. Bu yaklaşım, C++14'te güvenliydi.
Artılar: Standart bellek yönetimi, sızıntıları önler, eski kod tabanları arasında yaygın olarak desteklenir.
Eksiler: Dinamik tahsis yükü ve gösterici dolayısıyla ek yük getirir; belirleyici düşük gecikmeyi gerektiren kernel bağlamlarında sınırlayıcıdır; ayrıca CPU önbelleğini parçalara ayırır ve tahsis hataları için istisna işleme dikkate alınmasını gerektirir.
Çözüm 2: Dış Parametre Başlatma
Fabrika işlevine, yerinde başlatılması için çağıran tarafından tahsis edilmiş bir nesneye referans geçirmeyi düşündük.
Artılar: C++ standart versiyonuna bakılmaksızın sıfır kopyalama garantisi; yığın tahsisi yok; taşınamaz türlerle uyumludur.
Eksiler: Akıcı API stilini yok eder (auto h = create(); şimdi Handle h; create(h); olur); başlatmadan önce kullanım riskini artırır ve standart algoritmalar ve aralık tabanlı for döngüleriyle uyum sağlamakta zorluk çıkarır.
Çözüm 3: C++17 Garanti Edilen Kopyalama Elision'dan Yararlanma
Fabrikayı, prvalue'yı değerle döndürecek şekilde yeniden yapılandırdık, zorunlu elision'a dayanarak doğrudan çağıranın belleğine prvalue'yi oluşturduk.
Artılar: Yığın kullanımını ortadan kaldırır; değer semantiğini korur; derleme zamanında sıfır maliyetli soyutlama sağlar; hareket/kopyalama konstrüktörleri var olmamalı ya da erişilebilir olmamalıdır.
Eksiler: Tamamen saf rvalue'lara uygulanır (mevcut isimli değişkenleri döndüremezsiniz); C++17 desteğine sahip bir derleyici gerektirir; inşaat sırasında istisna işlemleri hakkındaki ince farklılıkların anlaşılması gerekir.
Çözüm 3'ü seçtik çünkü fabrika, saf prvalue'ler üreten taze geçici nesneler oluşturuyordu ve bu, garanti edilen elision senaryosuyla mükemmel bir uyum sağlıyordu. Bu, tutucuların kesinlikle taşınmaz kalmasını sağlarken, ergonomik değer semantiğini ve auto bildirimleriyle uyumu korudu.
Sürücü, binlerce eş zamanlı bağlantı için mikro saniye ölçeğinde başlatma süresi ile gönderildi. Montaj denetimi, tutucunun hiç bir yeniden yerleştirme veya kopyalama kodu olmadan doğrudan çağıranın yığın çerçevesinde inşa edildiğini belirtti. Tip sistemi, inşa sırasında kaynak güvenliğini sağladı ve sıcak yoldan yığın rekabetini tamamen ortadan kaldırdık.
Garanti edilen kopyalama elision, işlev içinde isimli dönüş değerlerine (lvalue) uygulanır mı yoksa yalnızca prvalue ile mi sınırlıdır?
Garanti edilen kopyalama elision, yalnızca prvalue'lara (saf rvalue), yani adı olmayan dönüş ifadesindeki geçici nesnelere uygulanır. İsimli Dönüş Değerleri Optimizasyonu (NRVO) hâlâ isteğe bağlı bir derleyici optimizasyonudur; geniş çapta uygulanmış olmasına rağmen, yapıcıya erişim veya yan etkilerle ilgili aynı garantileri vermez. Bir aday, isimli yerel bir değişkeni döndürmeye çalışır ve bunun garanti edilen elision'ı tetikleyeceğini varsayıp, silinmiş hareket konstrüktörüne sahip olursa, program kötü biçimlenmiş olur çünkü isimli değişkenler lvalue olup, hareket/kopyalama işlemlerini gerektirir, derleyici isteğe bağlı NRVO uygulamadığı sürece bu zorunlu değildir.
Açıkça silinmiş kopyalama ve hareket konstrüktörleri olan bir sınıf, garanti edilen kopyalama elision kuralları altında işlevden değerle döndürülebilir mi?
Evet. C++17'de, döndürülen ifade bir prvalue ise (örn., return MyClass{};), kopyalama ve hareket konstrüktörleri başlatma için hiç düşünülmez. Nesne doğrudan çağıranın belleğinde inşa edildiği için, silinmiş konstrüktörler kullanımda değildir ve derleme hatalarına neden olmaz. Ancak, böyle bir türde isimli bir değişken döndürmeye çalışmak başarısız olacaktır çünkü bu işlem konsept olarak lvalue'yi dönüş yerine taşımayı gerektirir; bu, silinmiş hareket konstrüktörünü çağırır ve kötü biçimlenmiş bir programa yol açar.
Garanti edilen kopyalama elision, istisna güvenliği ile nasıl etkileşir, özellikle yığın açılmasında prvalue geçici nesnenin ömrü ile ilgili?
Garanti edilen kopyalama elision altında, hedef nesnenin ömrü başlamadan önce ayrı bir geçici nesne oluşturulmaz. Prvalue, doğrudan nihai hedefinde malzeme haline gelir. Sonuç olarak, prvalue'nin inşası sırasında bir istisna meydana gelirse, yığın açılım mekanizması, yok edilmesi gereken ayrı bir geçici ile karşılaşmaz; bunun yerine kısmen inşa edilmiş hedef nesneyi görür. Bu, çağıran açısından, nesnenin tamamen inşa edilmiş ya da hiç yok olduğu anlamına gelir, bu da istisna güvenliği garantilerini basitleştirir ve hedef nesnenin ömrü resmi olarak başlamadan önce terkedilmiş bir geçici nedeniyle çift yok edilme veya kaynak sızıntısı oluşmasını engeller.