Haftalık C++ 16 – std::variant

Merhaba arkadaşlar, bu yazımda daha önce std::optional ile başladığımız ve birbirleri ile ilintili olduğunu düşündüğüm ikinci kabiliyetten bahsedeceğim, std::variant.

std::optional ve benzeri diğer kabiliyet yazılarıma aşağıdaki bağlantılardan ulaşabilirsiniz:

  1. std::optional
  2. std::variant
  3. std::any

Geriye sadece std::any kalmış oluyor, o kabiliyete ayrı bir yazıda yer vereceğim. Peki std::variant nedir?

Tek bir cümle ile std::variant’ı özetleyecek olursak, “tip güvenli union’lar“, ibaresi union yapılarını bilenleriniz için yeterli olacaktır. union yapılarını bilmeyenleriniz için ise kısaca, union‘lara da bir göz atalım. Bu sayede std::variant gibi yapının nereden geldiğini ve hangi sıkıntılara derman olduğunu anlamamız daha kolay olacaktır diye umuyorum.

Birlik (“union”):

Aslında union (birlik) yapısı C programlama dili ile de sunulmaktaydı. Temel olarak bu yapı ile birlikte belirli ve sabit bir bellek alanına, farklı zamanlarda, farklı veri tiplerinde verileri girebilmeyi sağlayan yapıdır. Bunun ile birlikte bellekten de tasarruf sağlanabiliyordu. Bu yapıların nesnelerini oluşturabildiğimiz gibi işaretçilerini de oluşturabiliyoruz. Bu yapıyı oluşturan değişkenlere, isimleri ile erişebilirsiniz. Bildiğimiz struct yapılarından farklı olarak, union‘ı oluşturulan değişkenlerin her biri için yer ayrılmaz, ilgili değişkenlerin en büyüğü kadar yer ayrılır. Hemen bir örnek üzerinden bunu inceleyelim:

Bu değişkenlerin herhangi birisi için veri girilebilir ama hepsi için girilemez. Bu sebeple eğer union kullanacaksanız bile, kullanılan tipi ayrıca bir değişken veya mekanizma ile takip etmelisiniz (ör. şu an float tutuluyor ya da int). Yukarıdaki örnek için aşağıdaki kullanım bu anlamda daha güvenli bir kullanım sağlayacaktır (buna “tagged unions” da deniliyor). Peki burada nasıl bir problem olabilir. Ör. instance nesnesi içerisindeki mCharValue değişkenine bir değer atadığınız zaman bu yapıdaki sadece bir byte kullanılmış olacak ve diğer üç byte boş/tanımsız kalacaktır. Bu da farklı değişkenlere erişimlerde beklenmeyen problemlere yol açabilir.

union ların yaygın olarak kullanıldığı durumlardan birisini de aşağıda görebilirsiniz:

C’den farklı olarak C++ union yapıları içerisinde private, pulic ve protected tanımlamaları yapılabilir. Ayrıca C++ 11 ile birlikte yapıcı/yıkıcı ve kopya yapıcı metotları veya atama operatörü içeren tipler de union‘lar ile birlikte kullanılabiliyorlar (ör. std::string).

Sonuç olarak union‘lar ile ilgili yaşadığımız/yaşayabileceğimiz problemleri aşağıdaki gibi sıralayabiliriz:

  • İçerdiği veri tipinden farklı bir veri tipindeki değişkene erişmeye kalkarsanız, karşılaşacağınız davranış hoşunuza gitmeyebilir 🙂 Buna ilişkin durumları yukarıda gördük.
  • İçerdiği veri tipini değiştirdiğiniz zaman ilgili nesneye ilişkin yapıcı veya yıkıcılar çağrılmaz. Hemen https://en.cppreference.com/w/cpp/language/uniondan bir örnek üzerinden inceleyelim:

Bu konu ile ilgili daha detaylı bilgi edinmek için kaynaklar kısmında verdiğim, referanslara bir göz atabilirsiniz.

Sonuç olarak std::variant, bellek belir bir miktarda yer kaplayan (ki bu alanı boost::variant‘ın aksine dinamik olarak almıyor) ve tanımlanan tipler arasında, bu alanın daha güvenli kullanılmasına olanak sağlayan mekanizma olarak özetleyebiliriz. Ayrıca eğer çok alt seviye bir kabiliyet geliştirmiyorsanız, union‘lar yerine std::variant‘ı tercih edebilirsiniz.

std::variant:

std::variant, programlama dünyasında, “sum type” denilen kavramın C++ dünyasındaki karşılığı. Ayrıca “tagged union”, “closed discriminated union” olarak da ifade edilebiliyor. Ne demek peki bu? Aslında kısaca (elbette arkasında oldukça derin bir teorik arka plan olduğuna eminim), aynı anda bir farklı tiplerden sadece birisini tutan tiplere verilen isim (ör. eğer bir tip aynı anda sadece A ya da B olabiliyor ise). Bunun bir benzerine de “product types” deniliyor. Bu da, birden fazla tipi içerisinde barındıran tiplere verilen isim (ör. struct, tuple, vb).

std::variant ile C++, belirlenmiş olan olası tip alternatiflerini içerebilen ve bunları daha güvenli bir şekilde kullanılmasını olanak sağlayan “sum type” mekanizmasını bizlere sunuyor.

std::variant, C++ 17 ile birlikte sunulmaya başlandı ve kullanmak için ‘<variant>’ başlık dosyasını eklemeniz gerekmekte. std::variant aslında arka planda şu anda aktif olarak kullanılan tipi tutar (bu anlamda ekstra bir bellek kullanımı vardır, ama tahmin edebileceğiniz üzere minicük), ayrıca içerisindeki değişkenlere ilişkin de indeksleme/numaralandırma yapılarak, bu değişkenlere bu indeksler üzerinde de erişim sağlanıyor (birazdan göreceğiz). Dinamik bellek kullanımı yapılmaz. Kullanılması olası bütün tipleri tanımlama sırasında belirlemeniz ve sağlamanız gerekiyor. Varsayılan olarak ilk tanımlanan tip aktif olarak atanır. Burada ayrıca aynı tipten birden fazla tutabilirsiniz (semantik olarak farklı anlamları ifade etmeleri için) ama boş std::variant tanımlamalarına ya da referans tipleri ya da dizi tiplerine izin verilmez (boş olarak tanımlamaya yönelik std::monostate yapısı tanımlanmış, bunun kullanımına da ileride değineceğim).

Peki hangi amaçlar için bu yapıyı kullanabiliriz. Burada bunları listeleyip, örnek kodları ise yazımın sonunda sizler ile paylaşacağım.

Kabiliyetlerin detaylarını sıralamadan önce hemen örnek bir kullanıma bakalım:

Kabiliyetler:

Şimdi std::optional‘da olduğu gibi, kalem kalem std::variant kullanımına ilişkin hususların üzerinden örnek kodlar ile birlikte geçelim:

  • std::variant nesnelerini nasıl oluşturabiliriz:
    • Öncelikli olarak std::variant varsayılan olarak sağlanan ilk elemanın, varsayılan yapıcısını çağırır ve onun tipi aktif olarak atanır,

    • Ya da ilgili değeri geçirerek de nesneyi oluşturabilirsiniz. Bu durumda, değere en iyi karşılık gelen tip seçilir:

    •  Eğer hangisinin seçileceği belli değil ise std::in_place_index ile açık bir şekilde hangi tipin seçilmesi gerektiği dikte edilebilir:

    • std::in_place_index ile “initialize list” nesnelerini de kullanabilirsiniz:

    • Eğer daha karmaşık veri tipleri kullanıyorsanız, bu durumda da std::in_place yapısı da kullanılabilir:

    • Varsayılan yapıcısı olmayan tipleri de desteklemek adına, std::monostate yapısı da bu kütüphane tarafından sunulmaktadır. Bu yapının temek amacı, std::variant nesnesinin, herhangi bir tipe ait bir değer içermediği durumları ifade etmektir. Nasıl yani? Mesela, varsayılan yapıcısı olmayan bir tipi de std::variant ile kullanmak istediğimizi düşünelim ve bu nesneyi de herhangi bir değer atamadan oluşturmamız gerekiyor ne yapacağız? Hemen bakalım:

  • std::variant tarafından tutulan değerlere nasıl erişebiliriz?
    • Tutulan değerin tipini öğrenmek için std::holds_alternative<>() API’sini kullanabilirsiniz:

    •  İlgili değere ulaşmak için, alternatifin tipini girerek get<TYPE>() API’sini kullanabilirsiniz. Aynı zamanda tip yerine indexi de girebilirsiniz get<INDEX>(). Burada eğer ilgili tip tanımlı değil ise derleme zamanında hata, eğer mevcut atanmış tipten farklı bir tipte veri sorgulamaya kalkarsanız da çalışma zamanında hata (“exception”) fırlatılır.

    •  get<>() API’si yanında hata fırlatmayan ve ilgili değere erişmeden kontrol yapmanıza olanak sağlayan get_if<>() API’sini de kullanabilirsiniz. Diğer API’den farkı ise bunun ilgili nesnenin işaretçisini parametre olarak alması:

  • Evet değerlere nasıl eriştiğimize baktıktan sonra şimdi de nasıl değiştirebileceğimize bakalım:
    • İlgili nesneye değeri atamak için atama operatörünü ya da emplace() API’sini kullanabilirsiniz. Ayrıca get ve get_if API’leri de referans ve işaretçi döndüğü için onlar da kullanılabilir:

  • Ayrıca çok kritik olmasa da, aynı tip iki std::variant nesnesini karşılaştırabilirsiniz. Aktif tiplerin farklı olması durumunda hangisinin aktif olduğuna göre karşılaştırma operatörleri dönüş sağlar:

  • std::variant‘ları kullandığınız durumlarda, ilgili nesnelerin barındırdıkları tiplere göre bazı işleri yapmak istersiniz. Bu işlevi de kolaylaştırmak adına STL’de bizlere std::visit API sini sunmakta. Bu API’nin temel amacı, kendisine geçirilen std::variant nesne/nesnelerini ilgili aktif tiplerine göre geçirilen fonksiyon tarafından çağrılması. Burada tabi ilgili fonksiyonun her bir tip için tanımlanmış olması önemli. Aksi halde derleme hatası alırsınız. Sanırım buna bir örnek üzerinden baksak daha iyi olacak 🙂

    •  visitor ile ayrıca ilgili nesnedeki aktif tipin değerini de değiştirebilirsiniz ama aktif tipi değiştiremezsiniz:

    • std::visit’in lambdalar ile de kullanımları mevcut. Burada sadece basit bir örnek verip, sizleri ilgili kaynaklara bakmaya davet ediyorum. Aşağıda basitçe ilgili nesnenin aktif tipine göre değeri standard çıktıya basan bir lambda örneğini görebilirsiniz. Benzer şekilde içeriği de değiştirebilirsiniz.

  • std::variant ayrıca taşıma operatörleri (std::move) ile de kullanabilirsiniz,
  • std::variant’lar her ne kadar bellekten ufak bir ödün versek de, çalışma zamanı performansında herhangi bir kayba yol açmıyor. Aşağıda ayrıca Ms Visual Studio 2017 için denediğim kod için aldığım boyut değerlerini görebilirsiniz:

sizeof string: 40
sizeof variant<int, string>: 48
sizeof variant<int, float>: 8
sizeof variant<int, double>: 16

  •  Aklınızın bir köşesinde durması gereken son konu ise, alternatif tiplerin oluşturulması ya da güncellenmesi sırasında hata fırlatılması durumu. Bunu da yine güzel bir şekilde gösteren aşağıdaki örnek kod üzerinden inceleyelim.

Yazımı tamamlamadan önce, çok temel bazı kullanımları içeren örnek kodları sizler ile paylaşacağım. Bunları Bartlomiej Filipekin sayfasından aldım, örnek kullanımları göstermesi açısından oldukça güzel. Bunlardan ilki poliformizme benzer bir mekanizmayı, miras mekanizmasını kullanmadan nasıl başarabileceğimiz gösteriyor:

Bir diğeri ise hata durumlarının kotarılmasına ilişkin:

Evet arkadaşlar, bu yazı ile birlikte std::variant maceramızın da sonuna geldik. Bir sonraki yazımda görüşmek dileğiyle.

Referanslar:

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

This site uses Akismet to reduce spam. Learn how your comment data is processed.