standart optional sınıfı

C++17 standartları ile standart kütüphanemizin bir eksiği daha tamamlandı. Artık bizim de bir optional sınıfımız var. Bu yazımızda std::optional sınıfını ayrıntılı olarak ele alacağız.

Programlamada sıklıkla karşımıza çıkan bir durum var: Bir koşul sağlandığında bir nesne oluşturup o nesneyi kullanmamız gerekiyor. Ama bu koşul sağlanmadığında ise bir nesneye ihtiyacımız kalmıyor dolayısıyla bir nesne oluşturmamız gerekmiyor. İşte std::optional sınıfı böyle durumlarda kullanılıyor.

std::optional<T> türünden bir nesne, programın çalışma zamanının belirli bir noktasında T türünden bir değer tutuyor ya da tutmuyor durumda olabilir. optional nesnesinin bir değere sahip olması kadar bir değere sahip olmaması da son derece doğal bir durum. std isim alanı içinde tanımlanan optional sınıf şablonunun bildirimi şöyle:

Burada T, optional nesnesinin tutabileceği nesnenin türü.

Peki böyle bir sınıfın gerçekleştirimi nasıl yapılabilir dersiniz? std::optional sınıfı türünden bir nesne aslında T türünden bir nesne için kullanılacak bir bellek alanına ve bir de bool değişkeni tutacak bellek alanına sahip. bool türden değişken std::optional nesnesinin T türünden bir nesneye sahip olup olmadığını gösterecek bir bayrak olarak kullanılıyor.

Herhangi türden bir nesneyi bool türden bir bayrak değişken ile birlikte bir sınıf oluşturacak şekilde sarmalayabiliriz. Sarmalanmış yapı içindeki bayrak öğesi, kullanıcı kodlara bir değerin tutulduğu ya da tutulmadığı konusunda bilgi verebilir. İngilizcede bu şekilde oluşturulmuş türlere “nullable types” deniyor. Böyle bir türden bir nesnenin kullanıcıları, bir yorum satırına gerek kalmaksızın, nesnenin bir değer tutup tutmadığını sorgulayabilirler. Programlama dünyasında opsiyonel türler yeni değil. Örneğin Haskel dilinde yer alan Data.maybe opsiyonel türlerin en eskilerinden biri. optional sınıfı 2003 yılından bu yana Boost kütüphanesinin kullanılan öğelerinden biri. Standart kütüphanenin optional sınıfının tasarımında da Boost kütüphanesinin deneyiminden büyük ölçüde faydalanılmış.

std::optional bir değer türü (value type) oluşturuyor. Yani kopyalama işlemleri ile aynı değere sahip farklı nesneler oluşturulabiliyor. Diğer taraftan bir optional nesnesi tutacağı değer için kendisi için ayrılmış bir bellek alanınını kullanıyor. Yani dinamik bir bellek alanı kullanmıyor. Aşağıdaki kodu derleyin ve ekran çıktısını yorumlamaya çalışın:

optional nesnelerinin oluşturulması

optional nesnelerinin oluşturulması için birden fazla yol var. Bunlardan biri, bir nesne hayata getirmeyen yani değer tutmayan (boş) bir optional nesnesi oluşturmak:

Yukarıdaki kodda optional<int> türünden op1, op2 ve op3 isimli nesneler hayata boş olarak getiriliyorlar. op3 için çağrılan kurucu işleve argüman olarak “std::nullopt” ifadesinin gönderildiğini görüyorsunuz. <optional> başlık dosyasında nullopt_t isimli bir boş sınıf (empty class) tanımlanmış. nullopt, bu boş sınıf türünden oluşturulan ve sabit ifadesi olarak kullanılabilen constexpr bir sınıf nesnesi. optional sınıfının nullopt_t türünden kurucu işlevi, nullopt sabiti ile çağrıldığında bu kurucu işlev boş bir optional nesnesi hayata getiriyor. Yine bir optional değişkenine bu sabitin atanması optional nesnesinin sarmaladığı değişkenin hayatını sonlandırıyor, böylece optional nesnesi boşaltılmış oluyor:

Bir optional nesnesini dolu olarak da hayata getirebiliriz. Aşağıdaki koda bakalım:

op1, op2, ve op3 nesnelerinin tanımında şablon tür argümanının kullanılmadığını görüyorsunuz.
Burada C++17 standartları ile dile eklenen ve popüler olarak CTAD (constructor template argument deduction) diye isimlendirilen özellik kullanılıyor. Bu mekanizma ile derleyici sınıfın kurucu işlevine gönderilen argümanın türünden hareketle şablon tür argümanının çıkarımını yapabiliyor. Yukarıdaki kodda derleyici op1 nesnesine ilk değer veren ifadeden hareketle op1 nesnesinin türünün çıkarımını

olarak yapıyor. Benzer şekilde op2 nesnesinin türü için

op3 nesnesinin türü için de

çıkarımları yapılıyor.
Şüphesiz optional nesnesini oluştururken şablon tür argümanını istediğimiz gibi belirleyebiliriz:

Ancak optional nesnesinin kurucu işlevine birden fazla değer gönderilecek ise bu durumda şablon tür argümanı belirtilse dahi çıkarım yapılamıyor. Bu yüzden optional sınıfının kurucu işlevine birden fazla argümanın gönderilmesi durumunda, tür çıkarımın yapılabilmesi için ilk argüman olarak in_place ifadesinin gönderilmesi gerekiyor. std::in_place standart <utility> başlık dosyasında tanımlanmış olan in_place_t isimli bir boş sınıf türünden constexpr bir nesnenin ismi. Bu tür boş sınıfların ve boş sınıf nesnelerinin varlık nedeni derleyicinin çıkarım yapmasına olanak sağlamak. Aşağıdaki koda bakalım:

make_optional işlevi

optional nesnelerini oluşturmanın bir başka yolu da make_optional isimli global yardımcı işlevi çağırmak. Bu işleve birden fazla argüman geçsek de artık in_place nesnesini işleve göndermek zorunda değiliz:

optional nesnelerinin boş olup olmadığını sınamak

Bir optional nesnesinin boş olup olmadığını yani bir değer tutup tutmadığını sınıfın operator bool ya da has_value isimli işlevleriyle sınayabiliriz:

Bir optional nesnesini nullopt değeriyle eşitlik/eşitsizlik karşılaştırmasına da sokabiliriz:

optional nesnesinin tuttuğu değere erişmek

optional nesnesinin tuttuğu değere erişmenin yine birden fazla yolu var. Sınıfın içerik operatör ve ok operatör fonksiyonları ile tutulan nesneye ya da o nesnenin öğelerine erişebiliriz. Ancak bu operatörlerin terimi olan optional nesnesinin boş olması durumunda tanımsız davranış (undefined behavior) oluşuyor. Böyle bir erişimde bir hata nesnesi gönderilmiyor (exception throw edilmiyor). Aşağıdaki koda bakalım:

Yukarıdaki koddan da görüldüğü gibi operator * işlevi referans döndürüyor.

Tutulan nesneye güvenli bir şekilde erişim gerçekleştirmek için öncelikle optional nesnesinin boş olmadığından emin olmalıyız:

value işlevi

Tutulan nesneye erişmenin bir başka yolu da sınıfın value isimli üye işlevini çağırmak. operator * işlevi gibi value işlevi de tutulan nesneye referans döndürüyor. Boş bir optional nesnesi için value işlevinin çağrılması durumunda, std::exception sınıfınından kalıtım yoluyla elde edilen std::bad_optional_access türünden bir hata nesnesi gönderiliyor:

value_or işlevi

Tutulan değere erişmenin bir başka yolu da sınıfın value_or isimli işlevini çağırmak. Bu işlev value işlevinden farklı olarak bir argüman alıyor ve optional nesnesinin boş olması durumunda kendisine gelen bu değeri döndürüyor:

value işlevinden farklı olarak value_or işlevi referans döndürmüyor:

optional nesnelerinin değerlerini değiştirmek

std::optional<T> sınıfı türünden bir nesnenin değerini değiştirmek için sınıfın atama operatörlerini kullanabiliriz. Atama operatörünün sağ terimi olan ifade

  • optional<T> türünden olabilir.
  • optional<U> türünden olabilir. (U türünden T türüne dönüşüm var ise)
  • T türünden olabilir.
  • U türünden olabilir. (U türünden T türüne dönüşüm var ise)
  • std::nullopt değeri olabilir.
  • {} ifadesi olabilir.

Aşağıdaki kodu inceleyelim:

emplace işlevi

optional sınıfının en önemli işlevlerinden biri emplace(). Bu işlev ile bir nesneyi kopyalama olmadan doğrudan optional nesnesi içinde yer alan bellek alanında hayata başlatabiliyoruz. emplace işlevi standart kütüphanedeki kap sınıflarının emplace işlevlerinde olduğu gibi mükemmel gönderim (perfect forwarding) mekanizmasından faydalanıyor. Dolu bir optional nesnesi için emplace işlevi çağrıldığında optional nesnesi tutmakta olduğu nesnesin sonlandırıcı işlevini (destructor) çağırıyor:

Yukarıdaki kodu derleyip çalıştırın. optional nesnesi dolu iken emplace işlevi her çağrıldığında önce A sınıfının sonlandırıcı işlevinin çağrıldığını daha sonra ise A sınıfının uygun kurucu işlevinin çağrıldığını göreceksiniz.

optional nesneleri tarafından kontrol edilen nesnelerin ömürleri

Bir optional nesnesinin hayatı bittiğinde optional nesnesi dolu ise hayata getirilmiş nesnenin sonlandırıcı işlevi çağrılır. Ancak aşağıdaki durumlarda da optional nesnesinin kontrol ettiği nesnenin sonlandırıcı işlevi çağrılır:
a) optional nesnesinin emplace isimli işlevinin çağrılması
b) optional nesnesine nullopt değerinin atanması
c) optional nesnesine {} ifadesinin atanması
d) optional nesnesinin reset işlevinin çağrılması (sınıfın reset isimli işlevinin çağrılmasıyla eğer optional nesnesi boş değil ise kontrol edilen nesnenin ömrü sonlandırılır.)

optional sınıfı ve taşıma semantiği

optional sınıfı taşıma semantiğini de destekliyor. Bir optional nesnesini başka bir optional nesnesine taşıyabiliyoruz. Bu durumda içerilen bir nesne var ise o da taşınıyor. Aşağıdaki kodu derleyip çalıştırın:

Yukarıdaki kodda, op1 nesnesinin taşınması ile op1‘in içerdiği A nesnesi taşınmış oluyor. Taşıma işleminden sonra op1 nesnesi dolu olsa da içerdiği nesne taşınmış durumda (moved-from state). İçerilen nesneye yeniden bir değer atamadan bu nesne yeniden kullanılmamalı.
İçerilecek nesneyi dışarıdan içeriye ya da içerilen nesneyi içeriden dışarıya taşımak da mümkün. Aşağıdaki koda bakalım:

Yukarıdaki kodu derleyip çalıştırdığınızda A sınıfının taşıyan kurucu işlevinin (move constructor) iki kez çağrıldığını göreceksiniz.

optional nesneleri ve karşılaştırma işlemleri

optional<T> türünden bir nesne
a) optional<T> türünden bir nesne ile
b) optional<U> türünden bir nesne ile (eğer T ve U karşılaştırılabilir türler ise)
b) T türünden bir ifade ile
d) U türünden bir ifade ile (eğer T ve U karşılaştırılabilir türler ise)
c) std::nullopt değeri ile

karşılaştırılabilir. Karşılaştırılan değerler optional nesnelerinin tuttuğu değerlerdir.
Boş bir optional nesnesi değeri ne olursa olsun dolu bir optional nesnesinden daha küçük kabul edilir.
İki boş optional nesnesinin karşılaştırılmasından true değeri elde edilir.
Aşağıdaki kodda yapılan karşılaştırma işlemlerini inceleyiniz:

optional<bool> nesneleri ile yapılan karşılaştırmalara özellikle dikkat edilmeli. Aşağıdaki koda da bakalım:

optional sınıfının kullanıldığı tipik durumlar

a) Bir işlevin optional sınıfı türünden bir değer döndürmesi. Bazı işlevler bir koşula bağlı olarak bir değer döndürebilirler. Ancak koşul sağlanmadığında döndürecek değerleri olmayabilir. Yani işlevin bir değer döndürmesi kadar döndürmemesi de doğaldır. Bu tür durumlarda işlevin geri dönüş değeri optional sınıfı türünden olabilir. Aşağıdaki kodu inceleyelim:

Yukarıdaki kodda tanımlanan to_int isimli işlev, bir std::string nesnesini bir tamsayıya dönüştürüyor.
Ancak işleve gönderilen yazının geçerli bir tamsayı ifade etmemesi durumunda işlevimizin geri döndüreceği bir tamsayı olamayacak. Bu yüzden işlevin geri dönüş değeri türünün

olarak seçildiğini görüyorsunuz. İşlev gelen yazıdan bir tamsayı elde edilemesi durumunda boş bir optional<int> nesnesi döndürüyor.
to_int isimli işlevi aşağıdaki gibi de tanımlayabilirdik:

b) Bir işlevin parametre değişkeninin optional sınıfı türünden olması. Bir işlevin bir parametresine, müşteri kodun bir değer göndermesi kadar değer göndermemesi de doğal bir durum ise işlevin parametre değişkeni optional sınıfı türünden yapılabilir. Bu durumda işlevi çağıracak kod bu parametreye ya bir değer ya da std::nullopt sabitini gönderebilir.

c) Bir sınıfın bir veri öğesinin optional sınıfı türünden olması.

Aşağıdaki kodda hem bir işlevin parametresinin hem de bir sınıfın bir veri öğesinin std::optional türünden olduğunu göreceksiniz:

Yukarıdaki kodda kişilerin isimlerini, temsil etmek amacıyla Name isimli bir sınıfın tanımlandığını görüyorsunuz. Sınıfın std::optional<std::string> türünden m_middle isimli veri öğesi kişilerin sahip olabileceği ya da sahip olmayacağı ikinci isimlerini tutması için tanımlanmış.

Share

Necati Ergin

C ve Sistem Programcıları Derneğinde eğitmen olarak çalışıyor.

Bunlar da ilginizi çekebilir

Kod Eklemek İçin Okuyun