BÇOM, SOLID, SSL derken haftalık C++ yazılarını unuttuğumu sandıysanız büyük yanılgı içerisindesin 🙂 Tabi, en son yazdığım yazının üzerinden bir kaç ay geçmiş olabilir ama bu seri devam edecek o kadar 🙂
Bu kısa yazımızda, C++ 17 ile birlikte gelen std::from_chars ve std::to_chars API’lerine bir göz atacağız. Bu API’ler bize ne kazandırıyor, daha önce bu anlamda sunulan mekanizmalar nelerdi? Bu API’ye neden ihtiyaç duyuldu ve tabi ki örnek kodlar. Öncelikli olarak mevcut metin/sayı ve benzeri dönüşümler için sunulan mekanizmalar ile başlayalım.
Metin/Sayı dönüşümleri
Şimdi hızlıca, metin sayı ve benzeri dönüşümler için kullanılabilecek mevcut API’lere madde madde, örnek kullanımları ile bakalım. Bu anlamda sırlayacağımız her bir API için genel kullanım amacı, özel durumları ve örnek kullanımlarından, yerel (“locale”) desteği olup/olmaması ve hata durumlarından da bahsedeceğiz. Bu hususlar ile hem bu API’leri hatırlamış oluruz hem de yeni gelen API ile farklarını daha net görebiliriz.
İlgili API’lere geçmeden önce dikkatinizi çekebileceğini düşündüğüm ve daha önce duymamış olabileceğiniz yerel (“locale“) desteğinden bahsetmek istiyorum.
Bu kabiliyet temelinde, karakter sınıflandırması ve metinlerin kullanımı, sayısal, parasal ve tarih/saat biçimlendirme ve ayrıştırma ve son olarak mesaj alımı için uluslararasılaşma desteğini bir takım ayarlamalar aracılığı ile kapsar. Windows ve diğer işletim sistemi kullanıcılarının bir çoğunun buna vakıf olduğunu düşünüyorum. Bu yerel ayarlar, I/O, “regular expressions” ve diğer bir takım C++ standart kütüphanelerinin davranışlarını etkiler. Bu kabiliyet ile ilgili kütüphane <locale>
başlık dosyası içerisinde toplanmıştır ve burada sıralayacağım bir takım API ve kütüphanelerin bir kısmı bunu kullanırken bir kısmı da kullanmamaktadır. Hızlıca bu kabiliyete örnek bir kod üzerinden 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
#include <iostream> // cin, cout vs. #include <sstream> #include <iterator> #include <locale> // Türkçe karakterler using namespace std; // std:: int main() { // Mevcut sistem yerel atamasini gosterir cout << "Current locale: " << std::locale().name() << endl; // Mevcut yerel ayarlari ile turkce karakterleri basamayiz cout << "Türkçe (ı, ğ, ü, ş, ö, ç) karakterleri kullanabiliriz.\n" << endl; // Mevcut yerel atamasini turkceye cevirelim std::locale::global(std::locale("tr_TR")); // Bu arada ilgili yerellestirme API'sinin C versiyonu da asagidaki gibidir. // Uygulamanin tamaminda gecerli olacak sekilde yeni yerellestirmeyi atar. //setlocale(LC_ALL, "turkish"); // Mevcut sistem yerel atamasini gosterir cout << "Updated locale: " << std::locale().name() << endl; // Turkce karakterlere sahip metinleri gosterebiliyoruz cout << "Türkçe (ı, ğ, ü, ş, ö, ç) karakterler kullanabiliriz.\n" << endl; // Bir de fransiz yerel ataması yapalim std::locale::global(std::locale("Fr_CH")); cout << "Updated locale: " << std::locale().name() << endl; // Simdi bu yerellestirme ayarlarinin sayilarda basimiza neler acabilecegine bakalim std::string de_double = "1.234.567,89"; std::string us_double = "1,234,567.89"; // Sayilari stream araciligi ile parse edelim std::istringstream de_in(de_double); de_in.imbue(std::locale("de_DE")); double f1; de_in >> f1; std::istringstream us_in(de_double); us_in.imbue(std::locale("en_US.UTF-8")); double f2; us_in >> f2; /* Ikisinin nasil farkli ayristirildigina bakalim */ std::cout << "Parsing " << de_double << " as double gives " << std::fixed << f1 << " in de_DE locale and " << f2 << " in en_US locale :) \n"; return 0; } |
Bu konu ile ilgili daha detaylı bilgiye ve sunulan diğer API’lere (ki oldukça fazla API var) https://en.cppreference.com/w/cpp/locale sayfasından ulaşabilirsiniz. Aslında şimdi düşünüyorum, bu konu da ayrı bir yazıyı hak ediyor sanki ne dersiniz 🙂 Bakalım hayırlısı.
Gelelim şimdi dönüşümler için kullanabileceğimiz API’lere:
- sprintf:
- Fonksiyon:
- int sprintf( char* buffer, const char* format, … )
- Çok meşhur printf() API’sinin metin formatlamak için kullanılabilecek kayınçosu. Bir de dosyalar için kullanılabilecek fprintf enişte de var ama o bu yazımızın heç konusu değil,
- Yerelleştirme desteği var ve geniş karakter desteği de sunuyor,
- Dönüşüm yanında formatlama,
- Yukarıdaki kabiliyetler neticesinde biraz işlem maliyeti var,
- Tampon aşımı/güvenlik riski mevcut,
- Bu API’ler’n ayrıca daha güvenli hale getirilmiş ve “_s” takısı ile isimlendirilen şekilleri de mevcut,
-
12345678910111213#include <stdio.h>int main (){char buffer [50];int n;int a{5};int b{3};n=sprintf (buffer, "%d plus %d is %d", a, b, a+b);printf ("[%s] is a string %d chars long\n",buffer,n);return 0;}
- Fonksiyon:
- snprintf:
- Fonskiyon:
- int snprintf(char *str, size_t size, const char *format, …)
- sprintf’in kuzeni. Ek olarak tampon boyutu alır ama kabiliyetin nasıl gerçeklendiği sisteme bağımlı oluyor genelde,
- Benzer maliyetler bu API için de geçerli,
-
12345678910111213141516171819#include <cstdio>#include <iostream>using namespace std;int main(){char buffer[100];int retVal, buf_size = 100;char name[] = "Max";int age = 23;retVal = snprintf(buffer, buf_size, "Hi, I am %s and I am %d years old", name, age);if (retVal > 0 && retVal < buf_size){cout << buffer << endl;cout << "Number of characters written = " << retVal << endl;}elsecout << "Error writing to buffer" << endl;return 0;}
- Fonskiyon:
- sscanf:
- Fonskiyon:
- int sscanf(const char *str, const char *format, …)
- Aslında sprintf’e benzeyen fakat, tabiri caizse, ters yönde çalışan bir API,
- sprintf API’sinde ilgili sayıları metine çevirirken, bu API ile birlikte metin olarak tutulan sayıları, verilen formata göre değişkenlere atılmasına olanak sağlanmakta,
- Önceki iki API gibi yerelleştirme desteği sunulmakta,
-
12345678910111213141516#include <stdio.h>#include <stdlib.h>#include <string.h>int main (){int day, year;char weekday[20], month[20], dtm[100];strcpy( dtm, "Saturday March 25 1989" );sscanf( dtm, "%s %s %d %d", weekday, month, &day, &year );printf("%s %d, %d = %s\n", month, day, year, weekday );return(0);}
- Fonskiyon:
- atol/atoi/atoll:
- Fonksiyon:
- int atoi( const char *str )
- long int atol(const char* str)
- long long atoll( const char *str )
- Verilen metinin temsil ettiği sayıya dönüşümünü gerçekleştirir. Bunu yaparken, ilk boşluk olmayan karaktere gelene kadar, boşlukları göz ardı eder,
- Eğer dönüştürülen değer, sınırlar dışında ise tanımlı bir dönüş değeri yoktur,
- Dönüş hiç yapılamaz ise 0 dönülür,
- Yerelleştirme ayarlarından etkilenir.
-
1234567891011121314151617181920212223242526#include <iostream>#include <cstdlib>int main(){const char *str1 = "42";const char *str2 = "3.14159";const char *str3 = "31337 with words";const char *str4 = "words and 2";int num1 = std::atoi(str1);int num2 = std::atoi(str2);int num3 = std::atoi(str3);int num4 = std::atoi(str4);// std::atoi("42") is 42std::cout << "std::atoi(\"" << str1 << "\") is " << num1 << '\n';// std::atoi("3.14159") is 3std::cout << "std::atoi(\"" << str2 << "\") is " << num2 << '\n';// std::atoi("31337 with words") is 31337std::cout << "std::atoi(\"" << str3 << "\") is " << num3 << '\n';// std::atoi("words and 2") is 0std::cout << "std::atoi(\"" << str4 << "\") is " << num4 << '\n';}
- Fonksiyon:
- strtol:
- Fonksiyon:
- long int strtol(const char *str, char **endptr, int base)
- Bu API, metin olarak verilen girdiyi, bir önceki API’de olduğu gibi ilk boşluk olmayan karaktere kadar ilerler ve sayı ise ilgili sayıyı (long int) döner ve geçirilen diğer işaretçiyi sayıdan sonraki ilk karakteri gösterecek şekilde günceller,
- Geçirilen sayı sınıfı parametresine göre ilgili değeri döner,
- Eğer dönüştürülen değer, sınırlar dışında ise LONG_MAX ya da LONG_MIN dönülür,
- Dönüş hiç yapılamaz ise 0 dönülür,
- Eğer geçirilen metin geçerli bir metin dizilimine sahip değilse ya da endptr geçerli değilse sonuç tanımsızdır,
- Yerelleştirme ayarlarından etkilenir,
-
1234567891011121314151617181920212223int main(){const char* p = "10 200000000000000000000000000000 30 -40";char *end;// Parsing '10 200000000000000000000000000000 30 -40':// '10' -> 10// ' 200000000000000000000000000000' -> range error, got 9223372036854775807// ' 30' -> 30// ' -40' -> -40std::cout << "Parsing '" << p << "':\n";for (long i = std::strtol(p, &end, 10);p != end;i = std::strtol(p, &end, 10)){std::cout << "'" << std::string(p, end-p) << "' -> ";p = end;if (errno == ERANGE){std::cout << "range error, got ";errno = 0;}std::cout << i << '\n';}}
- Fonksiyon:
- std::strstream:
- C++ 98 den bu yana artık kullanılması tavisye edilmiyor ve onaylanmamış olarak işaretlenmiş. Bunun yerine std::stringstream kullanılması tavsiye edilmekte, o sebeple bunun üzerinde durmayacağız.
- std::stringstream:
- Temel olarak >> ve << akış operatörlerini kullanarak, metin değişkenlerinden numerik veya benzeri verileri okuyup/yazmaya yararlar,
- Boşlukları göz ardı ederler,
- Dinamik bellek kullanımı vardır ve yukarıdaki API’lere göre daha fazla iş yüküne sahiptirler,
- Yerelleştirme ayarlarından etkilenir,
-
12345678910111213141516171819202122232425#include <iostream>#include <iomanip>#include <sstream>int main(){std::string input = "41 3.14 false hello world";std::istringstream stream(input);int n;double f;bool b;stream >> n >> f >> std::boolalpha >> b;// n = 41std::cout << "n = " << n << '\n'// f = 3.14<< "f = " << f << '\n'// b = false<< "b = " << std::boolalpha << b << '\n';// Kalan kısmını da standart girdi çıktı akışına yönlendirelim// hello worldstream >> std::cout.rdbuf();std::cout << '\n';}
- std::to_string:
- C++ 11 ile aramızı katılan ve numerik değeleri metine çeviren en son API’mizdir kendisi,
- Dinamik bellek kullanır ve yer kalmaması durumunda std::bad_alloc hatası fırlatabilir,
- Özellikle gerçek sayıların dönüşümünde beklenmedik çıktılar alabilirsiniz,
- Yerelleştirme ayarlarından etkilenir,
-
1234567891011121314151617181920212223242526272829303132333435363738394041#include <iostream>#include <string>int main(){double f = 23.43;double f2 = 1e-9;double f3 = 1e40;double f4 = 1e-40;double f5 = 123456789;std::string f_str = std::to_string(f);std::string f_str2 = std::to_string(f2); // Note: returns "0.000000"std::string f_str3 = std::to_string(f3); // Note: Does not return "1e+40".std::string f_str4 = std::to_string(f4); // Note: returns "0.000000"std::string f_str5 = std::to_string(f5);// std::cout: 23.43// to_string: 23.430000std::cout << "std::cout: " << f << '\n'<< "to_string: " << f_str << "\n\n"// std::cout: 1e-09// to_string: 0.000000<< "std::cout: " << f2 << '\n'<< "to_string: " << f_str2 << "\n\n"// std::cout: 1e+40// to_string: 10000000000000000303786028427003666890752.000000<< "std::cout: " << f3 << '\n'<< "to_string: " << f_str3 << "\n\n"// std::cout: 1e-40// to_string: 0.000000<< "std::cout: " << f4 << '\n'<< "to_string: " << f_str4 << "\n\n"// std::cout: 1.23457e+08// to_string: 123456789.000000<< "std::cout: " << f5 << '\n'<< "to_string: " << f_str5 << '\n';}
- std::stoi:
- Bir önceki API tam tersi yönde verilen metni ilgili sayı tipine dönüştüren ve C++ 11 ile gelen API leri kapsar,
- Hatalı bir girdi durumunda std::invalid_argument, sınır dışı bir veri durumunda ise std::out_of_range hatalarını fırlatır,
- Özellikle gerçek sayıların dönüşümünde beklenmedik çıktılar alabilirsiniz,
- Dinamik bellek kullanımı vardır,
- Yerelleştirme ayarlarından etkilenir,
-
12345678910111213141516171819202122232425#include <iostream>#include <string>int main(){std::string str1 = "45";std::string str2 = "3.14159";std::string str3 = "31337 with words";std::string str4 = "words and 2";int myint1 = std::stoi(str1);int myint2 = std::stoi(str2);int myint3 = std::stoi(str3);// Hata: 'std::invalid_argument' ilk bosluk olmayan karakter sayi degil// int myint4 = std::stoi(str4);// std::stoi("45") is 45std::cout << "std::stoi(\"" << str1 << "\") is " << myint1 << '\n';// std::stoi("3.14159") is 3std::cout << "std::stoi(\"" << str2 << "\") is " << myint2 << '\n';// std::stoi("31337 with words") is 31337std::cout << "std::stoi(\"" << str3 << "\") is " << myint3 << '\n';}
std::to/from_chars
Bir önceki başlık ile sanırım kullanabileceğiniz API ufkunuz genişlemiş ve her bir API kümesine ilişkin bir fikriniz olmuştur. Şimdi artık yazımızın konusu olan API’lere bakma zamanımız geldi.
Hemen ilk sorumuza dönelim. Bu kadar API varken neden bu API ler sunuldu? En önemli sebebi, bu API’ler yukarıdaki API’lere göre çok daha az bir işlemci yükü getirmekteler ve basittirler. Bu nasıl oluyor peki?Dinamik bellek kullanımları yok, herhangi bir hata fırlatmıyor (yine de hata dönme mekanizmaları mevcut), yerelleştirme ayarlarından etkilenmiyorlar. Bunların yanında, bellek aşımları kontrol altında ve en önemlisi, bu iki API arasında dönüşümlerde aynı değer garantisi sunulmakta (yani to_chars ile dönüştürdüğünüz noktalı sayı from_chars ile geri döndürdüğünüzde aynı olacak. Diğer API’lerde arada kayıplar olabiliyordu). Sanırım ne demek istediğimi anlatabildim 🙂 Sonuç olarak bu API’ler, daha alt seviye dönüşüm mekanizması ile yukarıdakilerden daha iyi bir performans sunuyorlar.
Şimdi bu iki API’ye yakından göz atalım:
Bu iki API’de <charconv> başlık dosyası aracılığı ile sunulmakta. to_char(), sayıyı metine döndürmek için, from_chars() metini sayıya geri döndürmek için kullanılır. Bu iki API de C+++ 17 ile birlikte sunulmaya başlandı, bu sebeple kullanmadan önce derleyicilerinizin bunu desteklediğinden emin olunuz
- Tam sayılar için:
- std::from_chars_result from_chars(const char* first, const char* last, TYPE & value, int base = 10)
- Noktalı sayılar için:
- std::from_chars_result from_chars(const char* first, const char* last, [float|double|long double]& value,
std::chars_format fmt = std::chars_format::general)
- std::from_chars_result from_chars(const char* first, const char* last, [float|double|long double]& value,
- Dönüş yapısı:
-
12345struct from_chars_result{const char* ptr;std::errc ec;};
-
- [first-last) dönüştürülmek istenen metin aralığı,
- value, dönüştürülme başarılı olması durumunda, dönüştürülen sayı,
- base, tam sayılar için sayı sistemi,
- fmt, noktalı sayılar için kullanılacak olan formatlama. Alabileceği değerler şu şekilde:
-
1234567enum class chars_format{scientific,fixed,hex,general = fixed | scientific};
-
- Basit tam sayı kullanımı:
-
123456789101112131415161718192021222324252627282930313233343536373839#include <charconv>#include <iostream>#include <string>int main(){const std::string str { "12345678 ay" };int value = 0;const auto res = std::from_chars(str.data(), str.data() + str.size(), value);// value burada artik 12345678 içeriyor// res.ec de std::errc{}'ye eşit. Bunların ne oldugunu asagida anlatıcamif (res.ec == std::errc()){std::cout << "value: " << value << ", distance: " << res.ptr - str.data() << '\n';}else if (res.ec == std::errc::invalid_argument){std::cout << "invalid argument!\n";}else if (res.ec == std::errc::result_out_of_range){std::cout << "out of range! res.ptr distance: "<< res.ptr - str.data() << '\n';}return 0;}#include <charconv>...const char* str = "12 ay";int value;std::from_chars_result res = std::from_chars(str, str+5, value);...
-
- Ondalıklı sayı kullanımına örnek:
-
1234567const std::string str { "16.78" };double value = 0;const auto format = std::chars_format::general;const auto res = std::from_chars(str.data(),str.data() + str.size(),value,format);
-
- İlgili API’nin döndüğü res yapısı içerisinde ptr sayı olarak kullanılmayan ilk karakteri, ya da aralığın sonunu (last argümanını), ec de işlem sonucunu içerir. Bu bağlamda işlemin başarılı olup/olmadığını aşağıdaki gibi kontrol edebilirsiniz. Burada bu değer bir bool tipi olarak kontrol edemezsiniz:
-
1234567891011121314if (res.ec != std::errc{}){... // Hata olusmus}// Asagidaki gibi bir kullanim yok// if (res.ec)// {// }// Asagidaki gibi de bir kullanim yok// if (!res.ec)// {// }
-
- Burada dönülen struct yapısından ötürü yapısal bağlamayı da kullanabiliriz. Hemen bir örneğe bakalım:
-
1234if (auto [ptr, ec] = std::from_chars(str, str+5, value); ec != std::errc{}){... // Hatalı bir durum oldu kontrol edelim}
-
- Hatalı dönüşüm durumunda res.ptr ilk geçirilen parametreyi içerir, res.ec de std::errc::invalid_argument olarak atanır. Geçirilen value değiştirilmez,
- Sınır aşımı durumunda ise res.ptr ilk sayı olmayan/formata uymayan karakter işaretçisini içerir ve res.ec de std::errc::result_out_of_range içerir,
- Tam sayılar için:
- std::to_chars_result to_chars(char* first, char* last,
TYPE value, int base = 10)
- std::to_chars_result to_chars(char* first, char* last,
- Noktalı sayılar için:
- std::to_chars_result to_chars(char* first, char* last, [float|double|long double] value)
- std::to_chars_result to_chars(char* first, char* last, [float|double|long double] value, std::chars_format fmt)
- std::to_chars_result to_chars(char* first, char* last,[float|double|long double] value, std::chars_format fmt, int precision)
- Dönüş yapısı:
-
12345struct to_chars_result{const char* ptr;std::errc ec;};
-
- [first-last) dönüştürülecek olan sayının saklanacağı metin aralığı,
- value, dönüştürülmek istenen sayı,
- base, tam sayılar için sayı sistemi,
- fmt, noktalı sayılar için kullanılacak olan formatlama, yukarıda vermiştim,
- precision, noktalı sayılar için kullanılacak olan çözünürlük
- Örnek tam sayı kullanımı:
-
12345678910111213141516171819#include <iostream>#include <charconv>#include <system_error>#include <string_view>#include <array>int main(){std::string str(10, ' ');if(auto [p, ec] = std::to_chars(str.data(), str.data() + str.size(), 1981); ec == std::errc()){std::cout << str << ", Size: "<< res.ptr - str.data() << " characters\n"}else{std::cout << "Error!\n";}}
-
- Örnek noktalı sayı kullanımı:
-
12345678910111213141516#include <array>#include <charconv>double pi = 3.14159265359;std::array<char, 128> buffer;auto [ptr, ec] = std::to_chars(buffer.data(), buffer.data() + buffer.size(), pi,std::chars_format::fixed, 2);if (ec == std::errc{}){std::string s(buffer.data(), ptr);// ....}else{// error handling}
-
Son olarak, örnek kullanımlardan da göreceğiniz üzere ve bir C++ komitesindeki kişinin dediği gibi, bu API’ler aslında direk kullanmaktan ziyade bunları altta kullanan daha anlamlı üst seviye servisler ile kullanılması gerektiği yönünde. Eee o zaman neden bunlarda standarda eklenmiş diye sorabilirsiniz. O da, bunların mevcut C++’da, “null-terminated” C metin API’lerini kullanmadan zor olmasından ötürü.
Performans
Gelelim, bu API’lerin ortaya konulmasının arkasındaki en önemli motivasyon olan performans olayına bakmaya. Bu konuda Bartlomiej Filipek, oldukça detaylı bir ölçüm yaparak sağ olsun paylaşmış ve bende burada, o sonuçları sizler ile paylaşıyorum. Bu ölçümler, mili saniye cinsinde, 1000 elemanlı vektör üzerinde 1000 tekrar yaparak ölçülmüş.
Ölçümün yapıldığı makine, Windows 10 x64, i7 8700 3.2 GHz. Derleyici ayarlamaları:
- GCC 8.2 – compiled with-O2 -Wall -pedantic, MinGW Distro
- Clang 7.0 – compiled with-O2 -Wall -pedantic,Clang For Windows
- Visual Studio 2017 15.8 – Release mode, x64
Bu değerlerin grafiksel gösterimine ise aşağıdan ulaşabilirisiniz:
Sonuçlara bakacak olursak, çoğu durumda yeni API’ler performans anlamında bekleneni veriyor. Tabi bunun için yukarıda bahsedilen ödünleri de unutmamak lazım. Sonuç olarak, temel metin/sayı dönüşümleri için bu API’leri kullanabilirsiniz. Bu arada, ölçüm için kullanılan koda da, ilgili yazarın sayfasından ulaşabilirsiniz.
Ben Yazılımperver, bir sonraki yazımda görüşmek dileğiyle, kendinize iyi bakın.
Kaynaklar
- http://en.cppreference.com/w/cpp/utility/to_chars
- http://en.cppreference.com/w/cpp/utility/from_chars
- https://www.fluentcpp.com/2018/07/27/how-to-efficiently-convert-a-string-to-an-int-in-c/
- https://www.fluentcpp.com/2018/07/24/how-to-convert-a-string-to-an-int-in-c/
- https://stackoverflow.com/questions/55875862/c17-purpose-of-stdfrom-chars-and-stdto-chars
- https://www.bfilipek.com/2018/12/fromchars.html
- https://www.youtube.com/watch?v=4P_kbF0EbZM
- https://dzone.com/articles/how-to-use-the-newest-c-string-conversion-routines