Haftalık C++ 19 – std::from_chars/to_chars

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:

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,
  • 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,
  • 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,
  • 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.
  • 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,
  • 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,
  • 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,
  • 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,

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

std::from_chars:

  • 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)
  • Dönüş yapısı:
  • [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:
  • Basit tam sayı kullanımı:
  • Ondalıklı sayı kullanımına örnek:
  • İ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:
  •  Burada dönülen struct yapısından ötürü yapısal bağlamayı da kullanabiliriz. Hemen bir örneğe bakalım:
  • 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,

std::to_chars:

  • Tam sayılar için:
    • std::to_chars_result to_chars(char* first, char* last,
      TYPE value, int base = 10)
  • 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ı:
  • [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ı:
  • Örnek noktalı sayı kullanımı:

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

 

Bir cevap yazın

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

Bu site, istenmeyenleri azaltmak için Akismet kullanıyor. Yorum verilerinizin nasıl işlendiği hakkında daha fazla bilgi edinin.