Evet, bir diğer yeni C++ kabiliyet yazımız ile birlikteyiz. Bir süredir farklı mecralarda gördüğüm ama bir türlü yazmak kısmet olmayan std::string_view sınfına bugün bir göz atacağız.
Bunu yaparken de öncelikle, bu sınıf öncesinde elimizde neler vardı, neden böyle bir sınıfa ihtiyacımız var, hangi durumlarda bunu kullanabiliriz ve hangi koşullarda kullanmak pek doğru değil gibi hususlara bu yazıda kısaca bakacağız.
Bu kabiliyet C++ 17 ile aramıza katıldı ve kullanmak için <string_view> başlık dosyasını eklemeniz gerekmektedir. Bu sınıftan önce, metin yönetimi ve karakter dizileri için std::string ve C den de bildiğimiz const char* dizilerimiz mevcuttu ve genel olarak kullanımlarına baktığımızda, aşağıdaki gibi fonksiyonlara geçirilip kullanılabilmekteydiler:
1 2 3 4 5 6 7 8 |
// 1) Eski C kullanimi void textAPICharArray(const char* input); // 2) C++ kullanimi void textAPIString(const string& input); // 3) C++ 17 ile string_view kullanımı void TakesStringView(std::string_view input); |
Aslına bakarsanız normal şartlarda bu kullanımların hiç bir sıkıntısı yok, ta ki farklı tipte veriler ile uğraşmak zorunda kaldığınızda, işin rengi değişmeye başlıyor:
- Örneğin, C++ kodu içerisinde 1 no lu bir API’ye std::string geçirme ihtiyacı olduğunda, burada c_str() API’sini çağırmanız gerekmekte,
- Benzer şekilde elinizdeki std::string kullanan 2 nolu gibi bir API’ye karakter dizisi geçirmek istediğinizde, çoğu durumda derleyici geçici bir string nesnesi oluşturulacaktır.
Yukarıdaki iki durum aslında std::string_view için olası en sık kullanımları teşkil etmekte. Daha fazla detaya girmeden önce, std::string_view‘a bir göz atalım.
std::string_view, salt-okunur karakter dizilerine (yazma hakkında olmadan), herhangi bir sahiplik olmadan erişim sağlayan bir sınıf olarak ifade edebiliriz. Bir diğer ifade ile, var olan bir karakter dizine, görünüm/erişim sağlayan bir sınıftır.
Bu sınıf genel olarak, karakter dizisi verisine bir işaretçi ve bu dizinin boyut bilgisini içerecek şekilde gerçeklenir.
1 2 3 4 5 |
// Olasi bir tanimlama, çok çok basitlestirilmiş string_view { size_t mLength; const char* mStrData; }; |
Bu sebeple, bu sınıfın kopyasını oluşturmak, bu sınıfın işaret ettiği veriyi kopyalamayacağı için (“shallow copy“) oldukça hızlı bir şekilde gerçekleştirmektedir. Bu anlamda str::string_view fonksiyonlara direk değer olarak geçirilmelidir (“pass by value“).
Bu sınıf aracılığı ile sunulan temel API’ler aşağıdaki gibi listelenebilir. Dikkat edeceğiniz üzere, bu API’lerin hepsi veriyi değiştirmeyen API’ler. Peki remove_prefix/remove_suffix‘e ne olacak diye aklınıza gelirse, aslında bu API’lerde de arkadaki veri değiştirilmez. Sadece işaretçi ve boyut bilgisi güncellenir, burası önemli. Aynı durum substr(), API’si için de geçerlidir ama ona daha detaylı bakacağız.
operator[]
at
front
back
data
size
/length
max_size
empty
remove_prefix
remove_suffix
swap
copy
(notconstexpr
)substr
– karmaşıklıkO(1)
,std::string
ile sunulan iseO(n)
. Bu API önemli, neden bu şekilde bir fark var, birazdan buna değineceğim.compare
find
rfind
find_first_of
find_last_of
find_first_not_of
find_last_not_of
- sıralama operatörleri:
==, !=, <=, >=, <, >
operator <<
Tam liste ve detaylı açıklamalar için std::basic_string_view adresine göz atabilirsiniz.
Bu sınıf ile ilgili dikkat edilmesi gereken en önemli nokta, işaret ettiği karakter dizisinin yaşam süresi/kapsamı üzerinde hiç bir etkisi yoktur ve bu sorumluluk tamamen kullanıcıdadır. Bunun bir şekilde kotarılması, kod bütünlüğü için önemli.
Yukarıdaki tanımlar ışığında, string ve benzeri referansı geçirilen verileri değiştirmeden sadece erişmek istiyorsanız. std::string_view kullanım için tercih edilebilir. Eğer altta yatan veriye erişim ihtiyacı hasıl olursa da, string nesnesine bu string_view nesnesi geçirilebilir.
Şimdi gelelim, yukarıda bahsettiğimiz iki sıkıntılı kullanımı std::string_view nasıl çözüyor. std::string_view, hem karakter dizisi işaretçisi hem de std::string alan yapıcılar barındırmaktadır. İlk durumda (1. no’lu API kullanımı), string_view, ilgili işaretçiyi saklar ve strlen() ile boyut bilgisi saklanır. İkinci durumda ise, zaten std::string‘in barındırdığı işaretçi ve boyut direk saklanır.
std::string_view‘ın boyut anlamında nasıl bir avantaj sağladığını https://skebanga.github.io/string-view/ sitesinde de detaylı bir şekilde verilen bir örnek kod üzerinden inceleyelim. Bu kod üzerinden normalde std::string ve benzeri karakter dizilerinin kullanımından ötürü ne kadar dinamik bellek alındığını ve std::string_view ile bunların ne kadar azaltıldığını göreceğiz.
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 <iostream> #include <string> using namespace std; // new operatorunu yeniden tanımlayalim void* operator new(size_t n) { cout << "[ " << n << " bytes kadar yer alindi]\n"; return malloc(n); } // karsilastirma metodumuz bool compare(const string& s1, const string& s2) { bool result = false; if (s1 == s2) { result = true; cout << '\"' << s1 << "\" ile \"" << s2 << "\"" << " aynı!\n" ; } else cout << '\"' << s1 << "\" ile \"" << s2 << "\"" << " ayni degil!\n" ; return result; } int main() { string str = "Girdi stringi."; compare(str, "test string 1."); compare(str, "test string 2."); compare(str, "test string 3."); compare(str, "Girdi stringi."); return 0; } |
Bu kodu ben çalıştırdığımda aşağıdaki çıktıyı alıyorum:
1 2 3 4 5 6 7 8 9 |
[ 39 bytes kadar yer alindi] [ 39 bytes kadar yer alindi] "Girdi stringi." ile "test string 1." ayni degil! [ 39 bytes kadar yer alindi] "Girdi stringi." ile "test string 2." ayni degil! [ 39 bytes kadar yer alindi] "Girdi stringi." ile "test string 3." ayni degil! [ 39 bytes kadar yer alindi] "Girdi stringi." ile "Girdi stringi." aynı! |
Sizin de göreceğiniz üzere toplamda 5 * 39 byte’lık bir yer alınıyor. Sadece bir kaç karşılaştırma için! Şimdi de aynı kodu std::string_view kullanacak şekilde güncelleyip bir göz atalım:
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 |
#include <iostream> #include <string> #include <string_view> using namespace std; // new operatorunu yeniden tanımlayalim void* operator new(size_t n) { cout << "[ " << n << " bytes kadar yer alindi]\n"; return malloc(n); } // Yeni karsilastirma metodumuz bool compare(string_view s1, string_view s2) { bool result = false; if (s1 == s2) { result = true; cout << '\"' << s1 << "\" ile \"" << s2 << "\"" << " aynı!\n" ; } else cout << '\"' << s1 << "\" ile \"" << s2 << "\"" << " ayni degil!\n" ; return result; } int main() { string str = "Girdi stringi."; compare(str, "test string 1."); compare(str, "test string 2."); compare(str, "test string 3."); compare(str, "Girdi stringi."); return 0; } |
Göreceğiniz üzere sadece compare metodunu güncelliyoruz ve bu durumda elde ettiğimiz çıktı da aşağıdaki gibi:
1 2 3 4 5 |
[ 39 bytes kadar yer alindi] "Girdi stringi." ile "test string 1." ayni degil! "Girdi stringi." ile "test string 2." ayni degil! "Girdi stringi." ile "test string 3." ayni degil! "Girdi stringi." ile "Girdi stringi." aynı! |
Göreceğiniz üzere artık sadece bir kere bellek alınıyor! Bu kullanımın sağlayacağı bellek ve performans kazancını size bırakıyorum. Bu kodta sadece str nesnesi için bir yer alınıyor ve diğer sabit karakter dizileri için herhangi bir yer alınmıyor.
Yukarıdaki örnek için bir diğer avantaj da aslında, farklı tipte karakter dizisi ve string tiplerinden kurtulmamız. Örneğin, önceki yazılarımda olduğu gibi QT kullanıyorsanız, yukarıdaki tek bir compare fonksiyonu için aşağıdaki fonksiyonların hepsini tanımlanmaya ihtiyaç duyabilirsiniz!
1 2 3 4 5 6 7 8 9 |
bool compare(const std::string& s1, const std::string& s2) bool compare(const std::string& s1, const char* s2) bool compare(const std::string& s1, const QString& s2) bool compare(const char* s1, const std::string& s2) bool compare(const char* s1, const char* s2) bool compare(const char* s1, const QString& s2) bool compare(const Qstring& s1, const std::string& s2) bool compare(const Qstring& s1, const char* s2) bool compare(const Qstring& s1, const QString& s2) |
std::string_view‘ın buraya kadar anlattığım kullanımları yanında performans anlamında daha göze çarpan avantaj sağladığı bir diğer durum da substr() ve benzeri API’lerin kullanımında ortaya çıkmaktadır.
Hemen bir örnek kod üzerinden buna da bakalım:
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 |
#include <iostream> #include <string> #include <string_view> using namespace std; // new operatorunu yeniden tanımlayalim void* operator new(size_t n) { cout << "[ " << n << " bytes kadar yer alindi]\n"; return malloc(n); } int main() { // Gercekten uzun bir metin string str = "coooookkkkkkkk uuuuuzzzzzzzzzuuuuuuuuunnnnnnnnnnn biiiiiirrrrr mmmeeeeeeettttttinnnnn"; // Eski yontem :) Bu durumda substr API'si yeni bir string olusturarak donuyor // Alt satiri kapatirsaniz yukaridaki bellek disinda herhangi bir bellegin alinmadığını göreceksiniz cout << str.substr(15, 28) << '\n'; // Yeni yontem: Hic bir kopya olusturulmuyor! string_view view = str; // Ayni sekilde ilgili API'ler yine, yeni bir string_view nesnesi donuyorlar cout << view.substr(15, 28) << '\n'; return 0; } |
Performans anlamında somut bilgi ve analizler için kaynaklar kısmındaki sitelere bir göz atabilirsiniz. Sizlere bu sınıfın doğru kullanımında neler kazandıracağı hakkında fikir verecektir.
Evet, sevgili yazılımperver dostlarım, bir C++ yazımın daha sonuna geldik. Bir sonraki yazımda görüşmek dileğiyle.
Kaynaklar
- https://www.bfilipek.com/2018/07/string-view-perf.html
- https://skebanga.github.io/string-view/
- https://www.modernescpp.com/index.php/c-17-avoid-copying-with-std-string-view
- https://isocpp.org/files/papers/N3762.html
- https://www.learncpp.com/cpp-tutorial/6-6a-an-introduction-to-stdstring_view/