std::optional, yığın tahsisi veya işaretçi kavramları olmadan boş değerleri temsil etmek için C++17'de tanıtıldı. Ancak, C++20'ye kadar, birden fazla isteğe bağlı dönen işlemi bir araya getirmek, has_value() veya operatorbool kullanarak ayrıntılı belirleyici kontroller gerektiriyordu. Bu belirleyici stil, derin iç içe geçmelere ve iş mantığını bulanıklaştıran "korkuluk piramidi" kod yapıları oluşturmaktaydı.
Bir isteğe bağlı değeri, kendileri de başarısız olabilecek bir dizi işlemin bir parçası olarak dönüştürmeye çalıştığınızda, sorun ortaya çıkar. C++20'de, geliştiriciler isteğe bağlıyı value() ile manuel olarak açmak, geçerliliğini kontrol etmek ve nullopt durumlarını açıkça iletmek zorundadır. Bu yaklaşım, hata yönetimini iş mantığıyla karıştırır ve gereksiz kod miktarını önemli ölçüde artırır.
Çözüm, C++23 ile and_then (flat_map), transform (map) ve or_else (geri kazanım) gibi monadik işlemlerle gelir. Bu yöntemler, çağrılabilir nesneleri kabul eder ve otomatik olarak kısa devre yapar: eğer isteğe bağlı kapatılmışsa, çağrılabilir hiç çağrılmaz ve boş durum iletilir; eğer açık ise, çağrılabilir açılmış değeri alır. Bu da açık branşlama veya manuel nullopt iletimi olmadan akıcı, açıklayıcı boru hatları oluşturmayı sağlar.
// C++20: Belirleyici iç içe geçme std::optional<int> parse(std::string s); std::optional<double> compute(int x); std::optional<double> result_cxx20(std::string s) { auto opt_i = parse(s); if (!opt_i) return std::nullopt; auto i = *opt_i; return compute(i); } // C++23: Monadik kompozisyon std::optional<double> result_cxx23(std::string s) { return parse(s) .and_then([](int i) { return compute(i); }) .transform([](double d) { return d * 2.0; }); }
Her bir doğrulama adımının std::optional<ValidationError> veya std::optional<Transaction> döndürdüğü bir mikroservisi düşünün. Özel zorluk, her biri başarısızlığı göstermek için nullopt döndürebilecek olan format kontrolü, son tarih doğrulaması ve bakiye onayı aracılığıyla bir kredi kartını doğrulamaktır. İş gereksinimi, herhangi bir başarısızlığın tüm işlemi kısa devre yapmasını sağlarken net denetim izleri sağlamayı gerektirir.
Çözüm 1: İç içe geçmiş if-demeleri. Her bir doğrulama aşaması için açık if (opt.has_value()) blokları yazın ve kontroller başarısız olduğunda manuel olarak nullopt döndürün. Artıları: Açık kontrol akışı hata ayıklama ile kolaydır ve yığın durumunun anlık görünürlüğünü sağlar. Eksileri: "Merdiven" girintisi piramidi oluşturur, nullopt iletiminde DRY ilkesini ihlal eder ve iş mantığını hata yönetimi ile sıkı şekilde bağlı hale getirerek yeni doğrulama adımları eklerken yeniden yapılandırmayı zorlaştırır.
Çözüm 2: Erken dönüş makroları veya sarıcı fonksiyonlar. Her bir doğrulamayı saran otomatik açma ve hata durumunda dönme işlevi olan TRY makroları tanımlayın ya da her bir doğrulama için özel yardımcı fonksiyonlar yazın. Artıları: Girinti seviyelerini azaltır ve hata iletimini merkezileştirir. Eksileri: Standart dışı uygulamalar, geliştiricilerden kontrol akışını gizler, hata ayıklamayı makro soyutlama katmanları aracılığıyla karmaşıklaştırır ve uygulanabilir ayrıntılarla tüm alan adını veya başlıkları kirletmeyi gerektirir.
Çözüm 3: C++23 monadik arayüzü. .and_then() kullanarak isteğe bağlı dönen adımları birbirine bağlayın, değer projeksiyonları için .transform() ve geri kazanım ile günlüğe kaydetme için .or_else() kullanın. Artıları: Açıklayıcı akış, matematiksel fonksiyon bileşimini yansıtır, ara değişkenleri ortadan kaldırır, tek sorumlulukta lambda'ları zorunlu kılar ve açıklanan dallar olmadan otomatik kısa devre yapar. Eksileri: C++23 derleyici desteği gerektirir, işlevsel programlama kalıplarıyla tanışık olmayan geliştiriciler için daha dik bir öğrenme eğrisi sunar ve lambda türetimi nedeniyle derleme sürelerini artırabilir.
Seçilen çözüm: C++23 monadik zinciri ile std::optional kullanımına geçmek. Ekip, bu yaklaşımın modern işlevsel programlama uygulamalarıyla uyumlu olduğunu ve ödeme modülündeki hata yönetimi gereksinimlerini yaklaşık %40 oranında azalttığını seçti. Açıklayıcı sözdizimi, iş analistlerinin iç içe koşullu blokları ayrıştırmadan doğrulama mantığını gözden geçirmesine olanak tanıdı.
Sonuç: Doğrulama boru hattı, her lambda'nın saf bir fonksiyonu temsil ettiği, izole olarak birim testi yapılabilir tek bir akıcı ifade haline geldi. Yeni doğrulama adımları eklemek, yalnızca başka bir .and_then() çağrısı eklemeyi gerektirdi, mevcut kodun yeniden yapılandırılmasını veya girinti seviyelerinin değiştirilmesini gerektirmedi. Sistem, dalda bir yük olmadan saniyede on bin işlem başarılı bir şekilde gerçekleştirdi ve kod tabanı, monadik adımların bileşenli doğası sayesinde %95 birim testi kapsama oranını sürdürdü.
std::optional::transform referansları nasıl işler ve çağrılabilir bir nesne tarafından döndürülen bir referansın kazara sarkan referanslar oluşturmasının nedeni nedir?**
std::optional::transform her zaman std::optional<std::decay_t<U> döndürür, burada U, çağrılabilirin dönüş türüdür. Eğer çağrılabilir T& döndürüyorsa, bozulma referansı kaldırır, böylece değerin bir kopyasını üretir. Ancak, eğer çağrılabilir bir işaretçi döndürüyorsa veya isteğe bağlı kendisi geçici (prvalue) içeriyorsa, adaylar sıklıkla dönüşüm işleminin, isteğe bağlı içindeki değerin ömrünü yalnızca transform çağrısı süresince uzatabileceğini gözden kaçırır.
Eğer çağrılabilir, isteğe bağlı değerin bir üyesine referans döndürüyorsa ve o isteğe bağlı bir geçici ise, referans tam ifade sona erdikten sonra sarkan hale gelir. Çözüm, çağrılabilirin nesneler için değerle döndüğünden emin olmak veya std::reference_wrapper'ı dikkatli bir şekilde kalıcı depolama ile kullanmak, asla geçicilerle değil, ömrü uzatacak şekilde sağlamaktır. Ayrıca, adaylar, transform'un çağrılabilirin sonucunu yeni isteğe bağlıya kopyaladığını, bu nedenle referans dönüşlerinin genellikle güvensiz olduğunu bilmelidir.
Neden std::optional::and_then çağrılabilirin std::optional döndürmesini gerektirirken, transform herhangi bir türü kabul eder ve hangi istisna güvenliği garantisi, kısa devre yapma davranışlarını ayırıyor?
Adaylar çoğunlukla bu iki yöntemi karıştırır çünkü her ikisi de değerleri eşleştirir, ancak and_then (monadik bağ) özel olarak iç içe geçmiş isteklileri düzleştirir ve std::optional<U>'yi dönüş türü olarak gerektirir, böylece std::optional<std::optional<U> sarmalamayı önler. transform sadece herhangi bir dönüş türünü std::optional<U> içinde sarar ve monadik bağ yerine fonksiyonel bir haritalama işlevi görür. İstisna güvenliğindeki kritik ayrım: Eğer çağrılabilir and_then'de atma yaptığında, istisna yayılır ve orijinal isteğe bağlı değişmez çünkü and_then yalnızca yeni isteğe bağlı nesnenin başarılı bir şekilde oluşturulmasından sonra etkin değeri değiştirir.
Ancak, transform yeni değeri doğrudan isteğe bağlı depolamaya inşa eder veya eski birimi taşır ve eğer çağrılabilir atma yaparsa, C++23 standardı, isteğe bağlıyı boş bir durumda (kapatılmış) bırakılacağını belirtir. Bu, transform'un yalnızca temel istisna garantisi sağladığı anlamına gelir, çağrılabilir noexcept değilse, oysa and_then etkin olarak güçlü bir garantiyi sağlar çünkü tamamen yeni bir isteğe bağlı döndürür ve kaynağı yeniden atama yapılmadan önce değiştirmeden bırakır. Adaylar genellikle atma yapan bir dönüşüm işleminin içerdiği değeri yok ettiğini gözden kaçırır.
Neye göre std::optional::or_else value_or'dan farklıdır ve neden geri dönüşümün tembel değerlendirilmesi, or_else'nin pahalı varsayılan inşa içeren performans kritik yolları için gerekli kılar?
value_or, isteğe bağlı açık olduğunda bile, argümanını hevesle değerlendirir ve varsayılan değerin yapılandırılması gerekmektedir kontrol gerçekleşmeden önce. or_else, bir çağrılabilir kabul eder (tembel değerlendirme) ve yalnızca isteğe bağlı kapalı olduğunda çağrıyı tetikler, yapılandırmayı gerçekten gerekli olduğu zaman erteler. Adaylar çoğu zaman bu hevesli ile tembel ayrımını gözden kaçırır ve isteğe bağlı özel bir değer içeriyorsa value_or(ExpensiveObject()) kullanır.
or_else kullanımının doğru yeri, inşayı ertelemektir: opt.or_else([]{ return ExpensiveObject(); }). Ayrıca, or_else, hata bağlamına erişim sağlamaya veya varsayılan bir değer sağlanmadan önce günlüğe yazmaya izin verir ki bu value_or'un yapamayacağı bir şeydir çünkü yalnızca önceden yapılandırılmış değeri kabul eder.