Merhabalar arkadaşlar, yeni bir haftalık C++ yazımız ile birlikteyiz. Bu yazımın konusu, C++ 17 ile birlikte dile dahil edilen std::optional yeteneği. Bu kabiliyete neden ihtiyacımız var, nerelerde kullanabiliriz gibi sorulara çeşitli kod örnekleri üzerinden giderek bakacağız. Bu yapı ile ilintili olarak std::variant ve std::any yapılarına da farklı yazılarımda değineceğim. O zaman hemen başlayalım ne dersiniz.
Öncelikli olarak neden böyle bir yapıya ihtiyacımız var ona bakalım. Bazı durumlarda, değişkenlerde anlamlı bir veri olmadığını ifade etmek istediğiniz emin olmuştur (komut satırından girilen opsiyonel argümanlar ya da girilse de girilmese olabilecek alanlar, göbek ismi gibi). Mevcut durumda, bunu yapmanın elbette bir takım yolları var. Bunu işaretçilerde yapmak nispeten kolay, nullptr ile yapabilirsiniz ama bu ilklendirilmediği anlamına mı gelir ya da anlamlı veri olmadığına mı? Diğer tipler için durum biraz daha farklı. Tamsayı değişkenler için -1 ve benzeri değerler kullanabilirsiniz, bu durumda da artık -1 kullanamazsınız. Bazı durumlarda -1 gibi bir sayı bulmanız da zor olabilir ya da her seferinde bu amaç için kullanabileceğiniz bir rakam bulmak zor olabilir. Ör. Özel yapılar ya da ondalık sayılarda ne kullanacağız? Ayrıca bu tarz kullanımlarda, fazladan veri var mı ya da anlamlı mı, kontrolleri yapmanız gerekebilir (ör. önce ilgili değişken geçerli mi değil mi kontrolü, sonrasında eğer geçerli ise değişken değerini almak gibi).
İşte std::optional, tam da bu noktada imdadımıza yetişiyor. Bu yapı ile birlikte, ilgili değişkenin olup olmadığını/herhangi bir değer içerip içermediğini daha anlamlı ve standart bir şekilde gerçekleştirebileceğiz. Bu kabiliyet çeşitli kaynaklarda “nullable types” olarak da geçmektedir. Giriş kısmında anlattıklarımızı özetleyecek olursa, hiçbir değer almayacak tipleri ifade etmek için, bir işlem sonucunda anlamlı bir dönüş olmayacağı durumlarda ya da fonksiyonlara bir değer geçirme ya da geçirmeme durumları söz konusu olduğunda kullanılabilir.
std::optional, diğer bir takım kabiliyet gibi, boost::optional’ı baz almakta, o sebeple eğer onu kullandıysanız, geçişiniz çok zor olmayacaktır (bu arada boost kullanımına ilişkin örnek bir kaynak ekledim). İlgili kabiliyeti kullanmak için #include <optional> başlık dosyasını eklemeniz yeterli. std::optional ek olarak herhangi bir dinamik bellek kullanımı yapmaz, aslında basit bir “wrapper” olarak da değerlendirilebilir. Genelde ilgili yapının kendi kaplayacağı alan yanında, fazladan sadece bir byte kadar bir alan kullanır.
Peki, hangi durumlarda bu yapıyı kullanmalıyız? Güzel bir soru. Bu anlamda kalem kalem olası kullanım alanlarından bahsetmeden önce, boost::optional sayfasında, bu konuda verilen yazıya bir göz atabilirsiniz. İlgili sayfada daha detaylı bilgi verilse de, kısa bir özetini burada verelim:
optional<T> yi ilgili T değeri olmaması, T’ye ilişkin bir değer olması kadar normal, açık ve geçerli bir sebebin olduğu durumlarda kullanılması tavsiye edilmektedir.
Bu yapıyı bir sınıf üyesi olarak, dönüş değeri olarak ya da fonksiyona geçirilecek olan bir parametre olarak kullanabilirsiniz.
Detaylara geçmeden önce hemen bir örneğe bakalım. Bu örnek kod içerisinde string’i int’e çeviren bir metoda bakacağız:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
#include <optional> #include <string> #include <iostream> // Ilgili string'i mumkunse int'e cevirelim std::optional<int> convertToInt(const std::string& s) { try { return std::stoi(s); } catch (...) { return std::nullopt; } } int main() { for (auto s : {"42", " 077", "merhaba", "0x33"} ) { // ilgili degerleri cevir bakalim std::optional<int> oi = convertToInt(s); if (oi) { std::cout << "The integer obtained from '" << s << "' is: " << *oi << '\n'; } else { std::cout << "The integer can not be obtained from '" << s << "'\n"; } } return 0; } |
Şimdi kalem kalem std::optional kullanımına dair hususların üzerinden, örnek kodlar ile birlikte geçelim isterseniz. Daha sonra bu kabiliyetleri içeren daha kapsamlı kodlara da bakarız:
- Herhangi bir zamanda optional<T> nesnesi ya bir değer içerir ya da herhangi bir değer içermez ki bu da std::nullopt ile ifade edilir ki bu da aslında std::nullopt_t tipindedir.
- Nasıl oluşturabiliriz?
- Herhangi bir değer içermeyen nesneleri aşağıdaki gibi oluşturabiliriz. Aşağıdaki iki kullanımda bir değer içermez ve yapıcı çağrılmaz
1 2 3 |
std::optional<int> inst1; std::optional<double> inst2(std::nullopt); |
-
- Bir değer veya farklı bir nesne ile oluşturmak için de aşağıdaki kullanımlar geçerlidir. Burada ayrıca tiplerden bahsetmeye gerek yok, std::optional bu çıkarımları yapabilir:
1 2 3 4 5 6 7 |
// Burada => optional<int> std::optional intSample{42}; std::optional<std::string> strSample{"hello"}; // Burada => optional<const char*> std::optional charArrSample{"hello"}; |
-
- Birden fazla parametre alan nesneler için ilgili nesneyi oluşturup geçirebilirsiniz. Ya da daha güzeli direk ilgili nesne ile birlikte oluşturabilirsiniz, bu sayede geçici bir nesne oluşturulmasından da kurtulursunuz. Bunun için de std::in_place yapısı kullanılmakta:
1 2 3 |
std::optional complexSample { std::complex { 3.0, 4.0 } }; std::optional<std::complex<double>> inPlaceInstance {std::in_place, 3.0, 4.0}; |
-
- Akıllı işaretçilerdeki gibi std::make_optional metodu da sunulmaktadır. Bu durumda, std::in_place kullanmanıza gerek yoktur:
1 2 3 |
// optional<double> auto sampleOpt = std::make_optional(3.0); auto sampleOpt2 = std::make_optional<std::complex<double>>(3.0, 4.0); |
-
- Yine konteynerler ile de kullanılabilir:
1 |
std::optional<std::vector<int>> vectorSample(std::in_place, {1, 2, 3, 4}); |
- Değere nasıl ulaşacağız?
- İlgili nesne içerisindeki değere ise value() ile ulaşabilirsiniz. Eğer ilgili nesne herhangi bir değer içermiyor ise o zaman std::bad_optional_access istisnası fırlatılır,
- Akıllı işaretçilere benzer kullanım da sunulmaktadır. * ve -> operatörler tanımlanmıştır ve bunlarda ilgili değerlere ulaşmak için kullanılabilirler. Bir önceki kullanımın aksine, eğer ilgili nesne bir değer içermiyor ise, o zaman herhangi bir istisna fırlatılmaz ve davranış tanımlı değildir. Yani dikkatli olmak lazım 🙂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
std::optional o{std::pair{42, "hello"}}; // p pair<int,string> nesnesini tutar auto p = *o; // 42 basılır std::cout << o->first; std::optional<std::string> o{"hello"}; // ”hello” basılır std::cout << *o; // ”hello” basılır std::cout << o.value(); // Nesneyi bosaltalim o = std::nullopt; // Tanımlı olmayan davranis std::cout << *o; |
-
- Son olarak value_or(varsayılanDeger) kullanımı da mevcut. Yani eğer ilgili nesne herhangi bir değer içermiyor ise varsayılanDeger dönülür.
1 2 3 4 5 |
// Bos bir nesne olusturalim std::optional<double> odouble; // Bos oldugu icin varsayilan olarak ifade edilen deger donulur std::cout<< "odouble " << odouble.value_or(10.0) << '\n'; |
- Değer var mı nasıl sorgulayacağız?
- Herhangi bir std::optional nesnesinin bir veri içerip/içermediğini has_value() metodu ile sorgulayabilirsiniz.
- Ayrıca operator bool() da tanımlı olduğu için, nesnesinin kendisini if bloğunda kontrol edebilirsiniz.
- Aşağıda bu kullanımları görebilirsiniz:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// std::optional<int> std::optional o{15}; // true if (o) ... // false if (!o) ... // true if (o.has_value()) ... |
- Mevcut değeri nasıl değiştirebiliriz?
- Atama (=) operatörü ve emplace() API si ile bunu gerçekleştirebiliriz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// o da henuz bir deger yok std::optional<std::complex<double>> inst; // optimal<int> tipi std::optional inst2{77}; inst = 42; // ilgili nesne tipi std::complex<double> olarak degisiyor inst = {9.9, 4.4}; // value becomes complex(9.9, 4.4) // Tip yine degisiyor :) inst = inst2; // Artik bir deger yok inst = std::nullopt; // Artik var inst.emplace(5.5, 7.7); // Yine yok inst = {}; |
-
- Ayrıca işaretçilerdeki gibi * operatörü ile de ilgili nesnelere değer atanabilir:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
std::optional<std::complex<double>> inst; // Hatali kullanim. Tanimli olmayan davranis *inst = 42; // Sikinti yok inst.emplace(2.2, 3.3); ... if (inst) { *inst = 88; *inst = {1.2, 3.4}; } |
-
- Atama yanında std::optional<> aynı zamanda taşıma mantıklarını da desteklemektedir. Bir diğer ifade ile std::move ile farklı bir nesne, bir diğer nesneye taşınabilir.
- Karşılaştırma operatörleri ile kullanabilir miyiz?
- Standart karşılaştırma operatörlerini nesneler ile kullanabilirsiniz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
std::optional<int> empty; std::optional<int> two(2); std::optional<int> four(4); // false / true basmak için std::cout << std::boolalpha; // true (four > two) std::cout << (four > two) << "\n"; // false (four < two) std::cout << (four < two) << "\n"; // false (four == two) std::cout << (four == 4) << "\n"; |
-
- Eğer her iki nesne içerisinde de değer yok ise == true döner. Diğer karşılaştırmaların hepsi false döner. Yalnız, nesnelerden birisi eğer std::nullopt ise yani yok ise farklı durumlar ortaya çıkabilir. İçerisinde değer olmayan nesne her zaman diğerlerinden az olarak kabul edilir:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
std::optional<int> empty; std::optional<int> two(2); // true empty == std::nullopt // false empty== 2 // true empty < two // false empty > two // true two == 2 // true empty < two |
- std::optional metot dönüş değeri olarak nasıl kullanılır? Yazının başında verdiğimiz kod parçası bu anlamda aslında güzel bir örnek.
- reset() metodu ile de nesne içerisindeki tutulan değer atılır ve boş hale getirilir.
Şimdi çok temel bazı kullanımları içeren örnek kodlara bakalım. Bunların ilki, referanslarda verdiğim bir kitaptan aldığım kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#include <optional> #include <iostream> using namespace std; class Name { private: string mFirst; // optional'in uye olarak kullanilmasi optional<string> mMiddle; string mLast; public: // optional'in parametre olarak gecirilmesi ve move semantics icin ornek kullanim Name(string f, optional<string> m, string l) : mFirst{ move(f) }, mMiddle{ move(m) }, mLast{ move(l) } { } friend ostream& operator << (ostream& strm, const Name& n); { strm << n.mFirst << ' '; if (n.mMiddle) { strm << *n.mMiddle << ' '; } return strm << n.mLast; } }; int main() { // Gobek ismi yok Name n { "Ayse", nullopt, "Kopar" }; cout << n << '\n'; // Gobek ismi var Name m{ "Ahmet", "Ekrem", "Uzar" }; cout << m << '\n'; } |
Diğeri de ilgili referans sayfasından. Bu iki örnek de sizlere std::optional kullanımı hakkında sizlere fikir verecektir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
#include <string> #include <functional> #include <iostream> #include <optional> // Factory benzeri yapilardan donus icin kullanima ornek std::optional<std::string> create(bool b) { if (b) return "Godzilla"; return {}; } // auto donus degeri ile kullanima ornek auto create2(bool b) { return b ? std::optional<std::string>{"Godzilla"} : std::nullopt; } // referans tipinde donus saglamak icin std::reference_wrapper kullanilabilir auto create_ref(bool b) { static std::string value = "Godzilla"; return b ? std::optional<std::reference_wrapper<std::string>>{value} : std::nullopt; } int main() { std::cout << "create(false) returned " << create(false).value_or("empty") << '\n'; // Yukaridaki metotlar ile donulen degerleri kosul kontrolleri icin kullanabiliriz if (auto str = create2(true); str) { std::cout << "create2(true) returned " << *str << '\n'; } if (auto str = create_ref(true); str) { // get() API si ile reference_wrapper tarafindan tutulan degere erisim std::cout << "create_ref(true) returned " << str->get() << '\n'; str->get() = "Mothra"; std::cout << "modifying it changed it to " << str->get() << '\n'; } } |
Bir sonraki yazımda görüşmek dileğiyle. Bol kodlu günler:
Kaynaklar:
https://en.cppreference.com/w/cpp/utility/optional
https://arne-mertz.de/2018/06/modern-c-features-stdoptional/
C++ 17 The Complete Guide, Nicolai M. Josuttis
https://www.fluentcpp.com/2016/11/24/clearer-interfaces-with-optionalt/
Harika Bir Tutorial Olmuş. Emeğine Sağlık
Güzel geri bildiriminiz için teşekkür ediyorum, faydalı olduysa ne ala.