Haftalık C++ 43 – {fmt} Kütüphanesi

Evet sevgili yazılımperver dostlarım, kısa bir aradan sonra, bir süredir yazılımlarımda kullandığım ve sizlerin de kullanmasını şiddetle tavsiye ettiğim bir kütüphaneye derinlemesine göz atıyor olacağız: {fmt} kütüphanesi. Yazı uzun, işlecek kabiliyet çok o zaman çok oyalanmadan başlayalım.

FMT {fmt} Kütüphanesi

FMT nedir? Geliştiricisinin ağzıyla bakacak olursak:

{fmt} C stdio ve C++ iostream kabiliyetlerine, hızlı ve emniyetli bir alternatif sunan açık kaynaklı bir formatlama kütüphanesidir.

FMT kütüphanesi Victo Zverovich tarafından geliştirilen bir kütüphanedir. Bu kütüphaneyi de kapsayacak kabiliyetler, C++ 20 ile birlikte, yeni formatlama kütüphanesi olarak geliştiricilere de sunuldu. Kütüphanenin günce reposuna aşağıdaki adresten ulaşabilirsiniz:

FMT Kütüphanesi GitHub Sayfası

Detaylara girmeden önce isterseniz {fmt}’ye ilişkin temel kabiliyetleri öncelikle bir sıralayalım:

  • Python benzeri modern formatlama yaklaşımı,

  • Yüksek performans (https://www.zverovich.net/2020/06/13/fast-int-to-string-revisited.html),

  • Açık kaynaklı bir projedir,

  • Geniş karakter ve yerelleştirme desteği,

  • stdio ve iostream’e göre tip emniyeti ve derleme zamanında da format kontrolü sunma,

  • Kullanıcı tanımlı tipler ile kullanım,

  • Yüksek taşınabilirlik. Güncel sürüm C++ 11’in temel bir takım kabiliyetlerine ihtiyaç duysa da, C++ 98 için 4.x sürümü de halen desteklenmektedir,

  • C++ 20 formatlama kabiliyetlerini, C++ 11 ile sunma,

  • Kütüphane ve başlık olarak projelere eklenebilmektedir.

Temel Kullanım

Bu başlık altında {fmt} kütüphanesine ilişkin temel kullanıma bakıyor olacağız. Bunu da yaparken örnekler üzerinden gideceğiz. İlerleyen satırlarda, formatlama ve daha detaylı hususlara da değinececeğim.

Kütüphane ile sunulan temel başlık dosyaları ile başlayalım.

“fmt/core.h”, başlık dosyası ile temel formatlama, derleme zamanı kontrolleri ve minimum bağımlılık ile sunulmaktadır,

“fmt/format.h”, başlık dosyası ile bütün formatlama ve yerelleşitirme kabiliyetleri sunulmaktadır,

“fmt/ostream.h”, “fmt/printf.h”, başlık dosyaları ile printf/std::ostream desteği ve ilgili API’ler sunulmaktadır.

Bunları sadece ekleyerek kullanabilmek için (“header only” kullanım için), bu başlık dosyalarından önce, “#define FMT_HEADER_ONLY 1” tanımını yapmanız gerekiyor.

FMT kütüphanesi API’leri ve ilgili tipler, fmt adres uzayı içerisinde sunulmaktadır. Bu kütüphane ile sunulan temel API’lere aşağıdaki tablodan ulaşabilirsiniz:

Formatlama API’leri:

API İsmiÇıktı/DönüşAçıklama
fmt::format()std::stringFormatlama çıktısını std::string olarak döner.
fmt::format_to()fmt::memory_buffer / fmt::wmemory_bufferBir önceki API’den farklı olarak std::string nesnesi oluşturmadan, bellekte yer alana bir alana çıktıyı yerleştirir.
fmt::sprintf()std::stringsprintf API’si kullanımına benzer şekilde formatlama yaparak std::string döner. Her ne kadar, bu sprintf formatlaması kullanılsa da, aynı şekilde tip emniyeti sunulmakta ve hatalı kullanılmakta exception atılır.

Çıktı API’leri:

API İsmiÇıktı/DönüşAçıklama
fmt::print()stdout / dosya(FILE*)Formatlama çıktısını standard output’a ya da dosyaya basar.
fmt::printf()stdoutFormatlama çıktısını printf API’si kullanımına benzer şekilde formatlama yaparak standard output’a basar. Her ne kadar, bu printf formatlaması kullanılsa da, aynı şekilde tip emniyeti sunulmakta ve hatalı kullanılmakta exception atılır.
fmt::fprintf()Dosya (FILE*) / stream (std::basic_ostream)Bir önceki API’nin dosya sistemine çıktıyı basan halidir.

Bu API’ler temel olarak iki argüman almaktadırlar: fmt ve args.

fmt, gösterilecek/formatlanacak olan metin ve bu metin içerisinde kullanılacak olan değiştirilebilir alanlardan oluşan ({} içerisinde) stringdir. args, ise formatlanarak {} ile belirtilen alanlara geçirilecek nesnelerdir. Şimdi bu API’lerden birinin (fmt::format) tanımına bakalım:

Bu noktadan sonra, fmt::print’i kullanıyor olacağım, bunun yerine aynı şekilde fmt::format da kullanabilirsiniz.


Parantez Yer Değiştirme Alanları: Çok fazla oyalanmadan hemen kullanıma geçebiliriz. Öncelikle, formatlamak için verdiğiniz metinler içerisindeki formatlanacak alanlar {} ile ifade edilir:

Hemen aklınıza şu gelebilir. Sevgili yazılımperver, ben metin içerisinde { veya } kullanmak istersem ne yapacağım? Şunu yapacaksınız, önüne aynında bir dane daha ekleyeceğiz. Ör. {{ ya da }} gibi 😉

Parantez kullanımının bir diğer güzelliği de, özel formatlama yapmadığınız müddetçe, temel tipler için cout kullanımına benzer bir şekilde kullanabilirsiniz. Hemen bakalım


Konumsal Argümanlar: Bildiğiniz üzere printf ve iostream ile bir argümanı birden fazla kullanma veya argüman olarak verdiğiniz sıradan farklı bir şekilde kullanma şansına sahip değildiniz. Bu her ne kadar basit formatlama kullanımlarında sıkıntı olmasa da, karmaşık ya da tekrarlı kullanımlarda hakikaten yorucu olabiliyor. fmt bu anlamda güçlü. Hemen örnek üzerinden gidelim:


İsimlendirilmiş Argümanlar: Ben sayılar, sıralar ile uğraşamam, argümanları isimlendirmek istiyorum derseniz. O da mevcut, fakat bunun bir maliyeti olacağını da unutmayın. Benzer şekilde, isimlendirilmiş argümanları da birden fazla yerde kullanabilirsiniz. Bu amaçlar fmt::arg(argumanIsmi, argumanDegeri) fonksiyonunu kullanabilirsiniz. Hemen örneğe bakalım.


Formatlama: Artık biraz formatlamanın detaylarına girebiliriz, bunun için de sunulan kabiliyetleri içerisinde barındıran ve geliştiricisinin verdiği bir örnek ile başlayalım. Aşağıdaki kod ile aslında formatlama namına, bu kütüphane ile neler yapabileceklerinizi (ya da günlük geliştirme faaliyetlerinizde) topluca görebilirsiniz. Formatlama tanımlaması, : noktadan sonra yapılıyor.

 

Ne der yukarıdaki kullanım?

Şunu der: 1.2345 kayan sayısını, sabit noktalı gösterim (presentation), noktadan sonra 2 rakam çözünürlükle yuvarla (precision) ve 10 karakter genişlik (width) içerisinde merkeze hizala (alignment) ve kalan boşlukları da * ile doldur (fill).

Peki çıktısı nedir: “***1.23***

Evet, zehri verdikten sonra şimdi, formatlama kullanımına biraz daha eğilebiliriz.

fmt için verdiğiniz metinler içerisindeki, yer değiştirme alanları dışındaki kısımlar aynen çıktıya yansıtılır. Parantez içerisindeki kısım : ile ayrılıyor. Bunlar argüman tanımlayıcısı ve formatlama spesifikasyonudur:

{argüman tanımlayıcısı : formatlama spesifikasyonu}

Parantez içerisindeki iki kısım da boş bırakılabilir. İki nokta sadece formatlama spesifikasyonu olduğu durumda eklenmelidir. Şimdi birkaç örmeğe bakalım.

Geçerli kullanımlar:

  • {}, bir sonraki argümanı, tipine göre kullanır,

  • , bir sonraki argümanı, tipine göre kullanır (: biraz fazladan),

  • {0}, ilk argümanı, tipine göre kullanır,

  • {argumanIsmi}, argumanIsmi isimli argümanı, tipine göre kullanır,

  • {argumanNumarasi}, argumanNumarasi sırasındaki argümanı, tipine göre kullanır,

  • {:*^10.2f}, sonraki argümanı, yukarıda belirttiğim şekilde formatlar,

  • {3:*^10.2f}, 4. argümanı, yukarıda belirttiğim şekilde formatlar.

Geçersiz kullanımlar:

  • {#:}, iki noktadan önce ya numara, ya argüman ismi ya da hiç bir şey olmalı,

  • {*^10.2f}, formatlamanın yapılabilmesi için iki nokta kullanılmalı.

Şimdi ortaya karışık bir kaç örneğe bakalım:

Mevcut printf formatlamalarının uyarlanması: Diyelim ki elimizde hazır printf kullanımları var, bunları hızlıca fmt kütüphanesine nasıl aktarabiliriz? Aslında çok zor değil, %’i silip : koymanız yeterli. Elbette etrafına {} koymayı unutmuyoruz. Hemen bir örneğe bakalım:


Dosyaya/Standart Hata Alanına Çıktı: Komut satırına bastık, peki dosyaya da basabilir miyiz? Elbette hemen bir kaç örneğe bakalım:


Belleğe Çıktı: Şimdiye kadar hep komut satırı ve std::string olarak formatlanmış çıktıları elde edebileceğimiz söylemiştik. Bunun yanında, bazı durumlarda, formatlanmış verileri bellekte de tutmak isteyebilirsiniz. Özellikle kısıtlı bellek kullanımlarında ya da çıktıların farklı yerler ile paylaşılması ihtiyaçları için fmt kütüphanesi buna ilişkin de API’ler sunmaktadır. Hemen bir örnek ile bu kabiliyete göz atıyor olalım.

Yukarıdaki kullanımda, dinamik bellek kullanımına ihtiyaç bulunmuyor ve bellek boyutunu belirtmenize de ihtiyaç kalmıyor.

Formatlama Grameri

İlgili olanlarınız için formatlama gramerine de elbette bakacağız. Aslına bakarsanız, sizlere tavsiyem, grameri anlamaya biraz vakit ayırmanız. Bu sayede neler yapabileceğinize ilişkin ufkunuz oldukça genişleyecektir. Öncelikle yer değiştirme alanını (replacement_field) tanımlayalım:

Sonra da, formatlama spesifikasyonuna bakabiliriz:

Hizalama opsiyonlarına bakalım:

OpsiyonAnlamı
'<'Sola hizalar, çoğu nesne için varsayılan hizalama budur.
'>'Sağa hizalar, sayılar için varsayılan hizalama budur.
'^'Merkeze hizalar.

İşaret (sign) opsiyonlarına bakalım:

OpsiyonAnlamı
'+'Hem pozitif hem negatif sayılar için işaretleri kullan.
'-'Sadece negatif sayılar için işareti kullan. Sayılar için varsayılan davranış budur.
boşluk ‘ ‘Pozitif sayılar için ön taraf boşluk, negatif sayılar için ise işaret.

Şimdi tip opsiyonlarına bakalım. Metin gösterim tipleri:

TipAnlam
's'String formatı. Bütün metinler için varsayılan formattır.
Tip Tanımlayıcı Yok İse's' ile aynı.

Karakter tip gösterim tipleri:

TipAnlam
'c'Karakter formatı. Karakter veri yapıları için varsayılan formattır.
Tip Tanımlayıcı Yok İse'c'ile aynı.

Öncelikle tam sayılar:

TipAnlam
'b'Binary format. Sayıları 2’lik sayı sistemine göre formatlar, “#” ile birlikte kullanılırsa, sayının önüne “0b” ekler.
'B'Binary format. Sayıları 2’lik sayı sistemine göre formatlar, “#” ile birlikte kullanılırsa, sayının önüne “0B” ekler.
'c'Karakter olarak basar.
'd'Tam sayı. Sayıyı 10’luk düzene göre formatlar.
'o'8 li format. Sayıyı 8’lik sayı düzenine göre formatlar.
'x'16’lık sayı formatı. Sayısı 16’lık formatta basar ve 9 dan büyük harfleri küçük basar, “#” ile birlikte kullanılırsa, sayının önüne “0x” ekler.
'X'16’lık sayı formatı. Sayısı 16’lık formatta basar ve 9 dan büyük harfleri büyük basar, “#” ile birlikte kullanılırsa, sayının önüne “0X” ekler.
Tip Tanımlayıcı Yok İse'd'ile aynı.

Kayan sayılar için tip opsiyonları:

TipAnlam
'a'Onaltılık kayan nokta biçimi. Sayıyı 16 tabanında “0x” öneki ve 9’un üzerindeki rakamlar için küçük harflerle yazdırır. Üslü belirtmek için ‘p’ kullanır.
'A'Önek için büyük harfler, 9’un üzerindeki rakamlar ve üssü belirtmek için kullanılması dışında ‘a’ ile aynıdır.
'e'Üs gösterimi. Üssü belirtmek için ‘e’ harfini kullanarak sayıyı bilimsel gösterimde yazdırır.
'E'Üs gösterimi. Ayırıcı karakter olarak büyük harfli bir ‘E’ kullanması dışında ‘e’ ile aynıdır.
'f'Sabit noktalı sayı.
'F'Sabit nokta. ‘f’ ile aynıdır, ancak nan’ı NAN’a ve inf’yi INF’ye dönüştürür.
'g'Genel biçim. Belirli bir p >= 1 kesinliği için, bu, sayıyı p anlamlı basamağa yuvarlar ve ardından sonucu büyüklüğüne bağlı olarak ya sabit nokta biçiminde ya da bilimsel gösterimde biçimlendirir. 1.
'G'Genel biçim. Sayı çok büyürse ‘E’ye geçmesi dışında ‘g’ ile aynıdır. Sonsuzluk ve NaN temsilleri de büyük harfle yazılmıştır.
Tip Tanımlayıcı Yok İseVarsayılan kesinliğin, belirli bir değeri temsil etmek için gerektiği kadar yüksek olması dışında, ‘g’ye benzer.

İşaretçiler için tip opsiyonları:

TipAnlam
'p'İşaretçi biçimi. Bu, işaretçiler için varsayılan türdür ve atlanabilir.
Tip Tanımlayıcı Yok İse'p' ile aynı.

Kullanıcı Tiplerinin Formatlanması

fmt kütüphanesinin sunduğu bir diğer güzellik de, var olan sınıf/veri yapılarınızı da, basit bir formatlayıcı yardımı ile bu kütüphanede kullanılabilir hale getirebilirsiniz. Hemen bir örneğe bakalım. Öncelikle bir veri yapısı tanımlayalım:

Veri yapımızı tamamladık. Şimdi, bu veri yapımızı fmt kütüphanesinin anlamlandırabilmesi için gerekli kabiliyetleri tanımlayalım.

Evet gelelim finale. Bu veri yapısını nasıl görünteleyebiliriz:

Zaman Formatlama

Yukarıda bir çok farklı formatlama örneğin baktıktan sonra şimdi biraz da, tarih ve saat çıktılarını nasıl formatlayabileceğimize bakacağız.

Renklendirme

fmt kütüphanesinin bir diğer güzel özelliği de, fmt/color.h başlık dosyasını ekleyerek ve basit bir güncelleme ile, print API’siyle komut satırına renkli, italik ve altı çizili metinler basabilmeniz. Hemen örneğe bakalım. Burada, şunu belirtmekte fayda var, italik ve bold gösterim için kullandığınız terminalin de bunu destekliyor olması lazım:

Emniyet

Kod satırını derleyip çalıştırdığınızda ne olur? Çoğu modern derleyici bu tarz bir kullanım gördüğünde sizleri derleme zamanında uyarır. Eğer uyarmıyorsa -Wformat veya muadili ayarı kontrol etmenizde fayda var. Sonuç olarak çoğu durumda yazılımınızın istemsiz bir şekilde kapandığını göreceksiniz.

ya da

Bunun yanında, fmt çalışma zamanında bazı kontroller yapar. Olası hatalarda da, “exception” fırlatılır.

Hatalı argüman indeksi girişi:

Hatalı tip tanımlayıcısı:

Başta da bahsettiğim gibi, fmt kütüphanesi derleme zamanında da bazı kontroller yapıyor. Bunlar genel formatın doğruluğunu derleme zamanında, çeşitli hatalar ile size sunar. Burada, main.cpp (119) ile aslında hatalı kullanımın olduğu satır gösterilmektedir.

Taşınabilirlik

C++ 11’in temel bir takım kabiliyetleri kullanması sebebi ile oldukça taşınabilir (GCC 4.8, CLang 3.4 ve VS 2015 üzeri derleyiciler ile kullanılabilmektedir). Bunun ile birlikte C++ 98 ve daha eski derleyiciler için 4.x sürümü de halen desteklenmektedir.

Bunun yanında normalde printf ile platform bağımlı çıktılar üretilen aşağıdaki gibi kullanımlarda, bir çok platform için aynı çıktıyı verir.

ki bu “inf”‘dir.

Performans

KütüphaneMetotSn Cinsinden Çalışma Zamanı
libcprintf1.04
libc++std::ostream3.05
{fmt} 6.1.1fmt::print0.75
Boost Format 1.67boost::format7.24
Folly Formatfolly::format2.23

{fmt}, yukarıda karşılaştırılan diğer yöntemlere göre en hızlısıdır, printf’den ~%35 daha hızlıdır.

Yukarıdaki sonuçlar, clang++ -O3 -DNDEBUG -DSPEED_TEST -DHAVE_FORMAT ile macOS 10.14.6’da tinyformat_test.cpp oluşturularak ve üç çalıştırmadan en iyisi alınarak oluşturulmuştur (yazarın yalancısıyım). Testte, "%0.10f:%04d:%+g:%s:%p:%c:%%\n" biçim dizesi veya eşdeğeri /dev/null’a gönderilen çıktıyla 2.000.000 kez doldurulur; daha fazla ayrıntı için bkz. source.

{fmt} is the fastest of the benchmarked methods, ~35% faster than printf.

The above results were generated by building tinyformat_test.cpp on macOS 10.14.6 with clang++ -O3 -DNDEBUG -DSPEED_TEST -DHAVE_FORMAT, and taking the best of three runs. In the test, the format string "%0.10f:%04d:%+g:%s:%p:%c:%%\n" or equivalent is filled 2,000,000 times with output sent to /dev/null; for further details refer to the source.

{fmt}, kayan nokta biçimlendirmesinde std::ostringstream ve sprintf’den 20-30 kata kadar daha hızlıdır (dtoa-benchmark), ve double-conversion, ryu  den de.

Diğer Dillerdeki Formatlama Kütüphaneleri İle Karşılaştırma

Öncelikle temel kullanımlara bir göz atalım:

C++:

C#:

Python:

Çıktı:

Şimdi de isimlendirilmiş argüman kullanımlarına bir göz atalım:

C++:

C#:

Python:

Çıktı:

Kaynaklar

Bir cevap yazın

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.