Wprowadzenie
Od długiego czasu następowały próby aby w
C++ utworzyć mechanizm do formatowania bezpieczniejszy niż
printf
z języka
C, ale wygodniejszy w użyciu niż
std::cout
.
Właśnie w
C++20 w końcu pojawił się
std::format
, a w
C++23 jeszcze funkcje umożliwiające formatowalne wyświetlenie na ekran:
std::print
czy
std::println
. To co weszło do standardu zostało zainspirowane
biblioteką fmt (licencja MIT). Jednakże biblioteka fmt nie weszła w całości do standardu
C++ przez co wciąż są argumenty aby jej używać dla chociażby kolorowania terminala, którego nie ma jeszcze w standardzie
C++. Poza tym programiści, trwający w projektach, które utknęły na starszym
C++ jeszcze nie będą długo mogli używać
std::format
.
Plan na ten artykuł jest następujący:
Standardowe formatowanie
Funkcje formatowania sprowadzają się do kilku szablonów funkcji. Jednym z nich jest:
template < typename ... Args >
std::string format( std::format_string < Args ... > fmt, Args && ... args );
Istnieją też wersje dla
std::wstring
, a także wersje umożliwiające wstrzyknięcie ustawień lokalizacji (locale) jako pierwszy argument (o nich później).
Poniżej przykładowy kod, który obrazuje najprostszy przypadek tworzenia sformatowanego tekstu:
#include <iostream>
#include <format>
#include <string>
int main()
{
std::cout << std::format( "Witaj {}!\n", "świecie" );
std::string text = std::format( "Czas zacząć naukę {}!\n", "C++" );
std::cout << text << std::endl;
}
co wydrukuje:
Witaj świecie!
Czas zacząć naukę C++!
Od
C++23 mamy jeszcze dostępną
std::print i
std::println przez co możemy uniknąć używania
std::cout:
#include <print>
int main()
{
std::println( "Mamy jeszcze większe możliwości w {}{}!", "C++", 20 );
}
Wydruk:
Mamy jeszcze większe możliwości w C++20!
Samo powyższe jeszcze nie zachwyca aż tak bardzo, ale przeanalizujmy sytuacje gdy mamy wiele typów danych i wiele możliwości formatowania (zgodnie ze
specyfikacją standardowego formatowania zainspirowaną
formatowaniem języka Python).
Wsparcie kompilatorów
Nie każdy kompilator obsługuje już wszystko, o czym piszemy w tym artykule. Możemy mieć dostępne
std::format, ale nie mieć jeszcze
std::println (jak np. na
GCC w wersji 13, będącym domyślnym kompilatorem
Ubuntu 24.04). Często pomoże nam odpowiednia flaga kompilatora (np. opcja
--std=c++23), mamy też możliwość sprawdzenia na stronie
C++ compiler support - cppreference.com. To, co udostępnia nasza biblioteka standardowa, można też sprawdzić w samym kodzie makrami z nagłówka
<version>:
#include <version>
#include <iostream>
int main()
{
#ifdef __cpp_lib_format
std::cout << "format: " << __cpp_lib_format << '\n';
#else
std::cout << "format: brak\n";
#endif
#ifdef __cpp_lib_print
std::cout << "print: " << __cpp_lib_print << '\n';
#else
std::cout << "print: brak\n";
#endif
}
W GCC 13 program wypisze
format: 202110
oraz
print: brak
, a w GCC 14 dodatkowo
print: 202211
.
Przykłady z tego artykułu były kompilowane w GCC 14 z opcją
--std=c++23. Wyjątkiem są te, które wymagają nowszej biblioteki standardowej niż w GCC 14: formatowanie zakresów, typ
? i
format_kind sprawdzono w Clangu 19 z libc++, a
dynamic_format z C++26 pod jego dawną nazwą
runtime_format. Jeśli nie masz pod ręką odpowiednio nowego kompilatora, możesz je uruchomić w serwisie
Compiler Explorer.
Budowa specyfikacji formatu
Zanim przejdziemy do szczegółów, warto zobaczyć, z jakich elementów składa się specyfikacja, czyli to, co wpisujemy w klamrach. Wszystkie elementy są opcjonalne, ale jeśli występują, to w poniższej kolejności:
W poniższym przykładzie użyliśmy kilku elementów naraz: znaku wypełnienia
*, wyrównania do prawej, wymuszonego znaku
+, szerokości 12, precyzji 3 i typu
f:
#include <print>
int main()
{
std::println( "{:*>+12.3f}", 3.14159 ); }
Dokładny zapis tej gramatyki jest na stronie
specyfikacji standardowego formatowania, a poszczególne elementy omawiamy w kolejnych rozdziałach.
Wyrównanie tekstu
W programach konsolowych miło się czyta logi, które są bardziej wyrównane kolumnowo. Dla strumieni stosowało się
std::setw (minimalna szerokość pola),
std::left,
std::right i
std::internal (w którym miejscu wstawić wypełnienie) oraz
std::setfill (znak wypełnienia).
W
std::format mamy możliwość ustawienia tego wszystkiego:
Dla osób znających strumienie poniższa tabela pokazuje, jak uzyskać to samo w obu światach (dla pola o szerokości 8):
Ważna różnica:
setw działa tylko na najbliższe wypisanie, ale
left,
right i
setfill zostają ustawione w strumieniu dla kolejnych wypisań, natomiast w
std::format każde pole ma własną specyfikację.
Zobaczymy to w poniższym przykładzie:
#include <format>
#include <iostream>
int main()
{
constexpr int date = 1683; std::cout << std::format( "|{:6}|\n", date ); std::cout << std::format( "|{:2}|\n", date ); std::cout << std::format( "|{:<7}|\n", date ); std::cout << std::format( "|{:>7}|\n", date ); std::cout << std::format( "|{:^7}|\n", date ); std::cout << std::format( "|{:*^6}|\n", date ); std::cout << std::format( "|{:.^8}|\n", date ); }
Przed pojawieniem się
std::format sposób wyrównywania tekstu odbywał się przy użyciu (
left,
right,
internal), co wpływa też na sposób wyświetlania liczb ze znakiem, zobaczymy to w poniższym kodzie (pionowe kreski dodałem, żeby było widać spacje):
#include <iostream>
#include <iomanip> using namespace std;
int main()
{
cout << '|' << setw( 10 ) << right << 1 << "|\n"; cout << '|' << setw( 10 ) << internal << 1 << "|\n"; cout << '|' << setw( 10 ) << left << 1 << "|\n"; cout << '|' << setw( 10 ) << right << - 2 << "|\n"; cout << '|' << setw( 10 ) << internal << - 2 << "|\n"; cout << '|' << setw( 10 ) << left << - 2 << "|\n"; cout << showpos;
cout << '|' << setw( 10 ) << right << 3 << "|\n"; cout << '|' << setw( 10 ) << internal << 3 << "|\n"; cout << '|' << setw( 10 ) << left << 3 << "|\n"; }
std::format nie ma odpowiednika
internal ze spacjami między znakiem a cyframi. Najbliższa jest flaga
0, która dopełnia zerami już za znakiem (a dopełnienie wskazane znakiem wypełnienia, np.
*>, ląduje przed znakiem). Znak liczby dodatniej wymusza
+ (odpowiednik
showpos), a spację w jego miejscu - flaga
" " (spacja):
#include <print>
int main()
{
std::println( "|{:10}|", - 2 ); std::println( "|{:<10}|", - 2 ); std::println( "|{:010}|", - 2 ); std::println( "|{:+010}|", 3 ); std::println( "|{:*>10}|", - 2 ); }
Precyzja liczb zmiennoprzecinkowych (strumienie)
Do ustawiania precyzji służy
std::setprecision, można pobrać domyślną precyzję przez zawołanie
std::ios_base::precision(). Ale już nieoczywistym jest jak się zachowują liczby w różnych reprezentacjach liczb zmiennoprzecinkowych. Mamy różne reprezentacje, jest
defaultfloat (domyślna), ale też
fixed,
scientific, ale też
hexfloat.
Najlepiej to zobaczyć na przykładzie (wydruk każdej linii jest w komentarzu obok):
#include <iostream>
#include <iomanip>
#include <numbers>
using namespace std;
int main()
{
cout << numbers::pi_v < double > << '\n'; cout << fixed << numbers::pi_v < double > << '\n'; cout << scientific << numbers::pi_v < double > << '\n'; cout << hexfloat << numbers::pi_v < double > << '\n'; cout << "Domyślna precyzja: " << cout.precision() << endl; cout << setprecision( 3 );
cout << defaultfloat << numbers::pi_v < double > << '\n'; cout << fixed << numbers::pi_v < double > << '\n'; cout << scientific << numbers::pi_v < double > << '\n'; cout << hexfloat << numbers::pi_v < double > << '\n' << endl; cout << setprecision( 2 );
cout << defaultfloat << - numbers::pi_v < double > << '\n'; cout << fixed << - numbers::pi_v < double > << '\n'; cout << scientific << - numbers::pi_v < double > << '\n'; cout << hexfloat << - numbers::pi_v < double > << '\n'; float f = numbers::pi_v < float > * 10e6f;
cout << defaultfloat << f << '\n'; cout << fixed << f << '\n'; cout << scientific << f << '\n'; cout << hexfloat << f << '\n' << endl; cout << defaultfloat << - f << '\n'; cout << fixed << - f << '\n'; cout << scientific << - f << '\n'; cout << hexfloat << - f << '\n' << endl; f /= 10e8;
cout << defaultfloat << f << '\n'; cout << fixed << f << '\n'; cout << scientific << f << '\n'; cout << hexfloat << f << '\n' << endl; cout << defaultfloat << - f << '\n'; cout << fixed << - f << '\n'; cout << scientific << - f << '\n'; cout << hexfloat << - f << '\n' << endl; }
Jak widać:
Ponadto wszystkie te ustawienia są "lepkie": raz ustawione zostają w strumieniu dla wszystkich kolejnych wypisań (dlatego w przykładzie po
hexfloat wracamy jawnie do
defaultfloat). Wyjątkiem jest
setw, które działa tylko na najbliższe wypisanie.
Precyzja w std::format
W
std::format precyzję podajemy po kropce, bezpośrednio w specyfikacji, a rodzaj zapisu wybieramy literką, ponadto literka ta dotyczy tylko jednego wyrażenia (nie jest "lepka"):
Ten sam przykład, tym razem z
std::format (przez
std::println):
#include <print>
#include <numbers>
int main()
{
constexpr double pi = std::numbers::pi_v < double >;
std::println( "{}", pi ); std::println( "{:g}", pi ); std::println( "{:f}", pi ); std::println( "{:e}", pi ); std::println( "{:a}", pi ); std::println( "{:.3g}", pi ); std::println( "{:.3f}", pi ); std::println( "{:.3e}", pi ); std::println( "{:.3a}", pi ); std::println( "{:.2g}", - pi ); std::println( "{:.2f}", - pi ); std::println( "{:.2e}", - pi ); std::println( "{:.2a}", - pi ); float f = std::numbers::pi_v < float > * 10e6f;
std::println( "{:.2g}", f ); std::println( "{:.2f}", f ); std::println( "{:.2e}", f ); std::println( "{:.2a}", f ); f /= 10e8f;
std::println( "{:.2g}", f ); std::println( "{:.2f}", f ); std::println( "{:.2e}", f ); std::println( "{:.2a}", f ); }
Kilka różnic względem strumieni, o których warto pamiętać:
Skąd tyle cyfr w samym {}? Dla liczb zmiennoprzecinkowych, gdy nie podamy ani precyzji, ani literki,
std::format korzysta z algorytmu
std::to_chars. Wybiera on najkrótszy zapis, po którego wczytaniu (
std::from_chars) dostaniemy dokładnie tę samą liczbę. Liczba cyfr zależy więc od wartości i od typu:
0.1 to po prostu
0.1, ale wynik
0.1 + 0.2 wymaga aż 17 cyfr, żeby odróżnić go od sąsiednich liczb typu
double. Ta sama liczba π ma dla
float 8 cyfr, a dla
double 16. Strumienie (i
printf
) domyślnie wypisują 6 cyfr znaczących - jest krócej i czytelniej, ale zapis nie gwarantuje odtworzenia tej samej wartości. Gdy chcemy krótszy wydruk, podajemy precyzję:
{:.6} albo
{:g}.
#include <print>
#include <numbers>
int main()
{
std::println( "{}", std::numbers::pi_v < double > ); std::println( "{}", std::numbers::pi_v < float > ); std::println( "{}", 0.1 ); std::println( "{}", 0.1 + 0.2 ); std::println( "{}", 1.0 / 3.0 ); std::println( "{}", 100.0 ); std::println( "{:g}", 0.1 + 0.2 ); std::println( "{:.17}", 0.1 ); }
Formatowanie liczb
Mimo pozorów formatowanie liczb przez strumienie (przed
std::format) zawiera bardzo wiele hacków, które warto znać jakby ktoś się przygotowywał do certyfikatu
C++ Professional Programmer (
C++ Institute).
Systemy liczbowe
Pierwszym co należy rozważyć są systemy liczbowe, strumienie obsługują: dziesiętny (
std::dec), ósemkowy (
std::oct) i szesnastkowy (
std::hex). Przykładowo:
#include <iostream>
int main()
{
std::cout << 64 << '\n'; std::cout << std::dec << 64 << '\n'; std::cout << std::oct << 64 << '\n'; std::cout << std::hex << 64 << std::endl; std::cout << std::showbase;
std::cout << std::dec << 64 << '\n'; std::cout << std::oct << 64 << '\n'; std::cout << std::hex << 64 << std::endl; }
W
std::format system liczbowy wybieramy literką:
d (dziesiętny, domyślny),
o (ósemkowy),
x (szesnastkowy), a dodatkowo
b (dwójkowy, którego strumienie nie miały - do tego używało się
std::bitset). Odpowiednik
showbase to znak
# (prefiks
0,
0x albo
0b). Wielka litera (
X,
B) daje wielkie cyfry i wielki prefiks. Flaga
0 dopełnia zerami po prefiksie:
#include <print>
int main()
{
std::println( "{} {:d} {:o} {:x}", 64, 64, 64, 64 ); std::println( "{:#d} {:#o} {:#x} {:#b}", 64, 64, 64, 64 ); std::println( "{:X} {:#X} {:B} {:#B}", 255, 255, 5, 5 ); std::println( "|{:#010x}|", 255 ); std::println( "|{:08b}|", 22 ); std::println( "|{:>#10x}|", 255 ); }
Odpowiedniki zebrane w tabeli:
Manipulatory systemu liczbowego są "lepkie", czyli obowiązują dla wszystkich kolejnych liczb w strumieniu, a literka w
std::format dotyczy tylko jednego pola.
Uwaga na liczby ujemne! Strumień z
std::hex wypisuje zapis bitowy (jak dla liczby bez znaku), a
std::format wypisuje znak minus i wartość bezwzględną. Jeśli chcemy zapis bitowy, musimy rzutować na typ bez znaku:
#include <iostream>
#include <format>
int main()
{
std::cout << std::hex << - 1 << '\n'; std::cout << std::format( "{:x}", - 1 ) << '\n'; std::cout << std::format( "{:x}", static_cast < unsigned >( - 1 ) ) << '\n'; }
Pozostałe flagi
Z czasów przed
std::format mamy kilka innych możliwości formatowania:
Dla strumieni wyglądają tak:
#include <iostream>
#include <iomanip>
int main()
{
std::cout << true << '\n'; std::cout << std::boolalpha << true << '\n'; std::cout << std::noboolalpha;
std::cout << 1.0 << '\n'; std::cout << std::showpoint << 1.0 << '\n'; std::cout << std::noshowpoint;
std::cout << std::hex << 255 << '\n'; std::cout << std::uppercase << 255 << '\n'; std::cout << std::showbase << 255 << '\n'; std::cout << std::dec << std::nouppercase << std::noshowbase;
std::cout << 3 << '\n'; std::cout << std::showpos << 3 << '\n'; }
A w
std::format:
#include <print>
int main()
{
std::println( "{}", true ); std::println( "{:d}", true ); std::println( "{}", 1.0 ); std::println( "{:#g}", 1.0 ); std::println( "{:#}", 1.0 ); std::println( "{:x}", 255 ); std::println( "{:X}", 255 ); std::println( "{:#X}", 255 ); std::println( "{}", 3 ); std::println( "{:+}", 3 ); std::println( "{: }", 3 ); }
Zestawienie odpowiedników:
NaN i nieskończoność
Liczby zmiennoprzecinkowe mogą mieć też wartości specjalne: nieskończoność (
inf) i "to nie jest liczba" (
nan). Domyślnie są wypisywane małymi literami, a wielka litera typu (
F,
E,
G) daje
INF i
NAN. Znak
+ działa jak dla zwykłych liczb. Flaga
0 dopełnia zerami tylko zwykłe liczby - dla
inf i
nan dopełnieniem są spacje. Zera nie są też używane, gdy jawnie podamy wyrównanie (np.
{:<010}):
#include <limits>
#include <print>
int main()
{
constexpr double inf = std::numeric_limits < double >::infinity();
constexpr double nan = std::numeric_limits < double >::quiet_NaN();
std::println( "{} {} {:+}", inf, nan, - inf ); std::println( "{:f} {:F}", inf, inf ); std::println( "{:g} {:G}", nan, nan ); std::println( "|{:010}|{:010}|", 3.5, inf ); std::println( "|{:<010}|", 3.5 ); }
Strumienie wypisują te wartości podobnie, ale wypełnienie zerami zachowuje się inaczej - dopełnia zerami także
inf:
#include <iomanip>
#include <iostream>
#include <limits>
int main()
{
constexpr double inf = std::numeric_limits < double >::infinity();
constexpr double nan = std::numeric_limits < double >::quiet_NaN();
std::cout << inf << ' ' << nan << ' ' << - inf << '\n'; std::cout << std::uppercase << inf << ' ' << nan << std::nouppercase << '\n'; std::cout << std::showpos << inf << std::noshowpos << '\n'; std::cout << std::setfill( '0' ) << std::setw( 10 ) << 3.5 << '\n'; std::cout << std::setfill( '0' ) << std::setw( 10 ) << inf << '\n'; }
Ustawienia lokalne
Separatory tysięcy i przecinek dziesiętny zależą od ustawień lokalnych. W
std::format włączamy je literką
L (przy precyzji za nią, np.
{:.2Lf}), a locale podajemy jako pierwszy argument albo ustawiamy globalnie. Bez
L locale jest ignorowane - inaczej niż w strumieniach, które zawsze używają locale ustawionego przez
imbue.
Zacznijmy od polskich ustawień z systemu. Nazwa locale zależy od systemu operacyjnego (na Linuksie np.
pl_PL.UTF-8, a w Windows
pl-PL - dlatego w przykładzie wybieramy nazwę makrem
_WIN32, które definiują kompilatory na Windows) i musi ono być zainstalowane - w przeciwnym razie konstruktor
std::locale rzuci wyjątek
std::runtime_error:
#include <iostream>
#include <format>
#include <iomanip> #include <locale>
#include <print>
int main()
{
#ifdef _WIN32
std::locale pl( "pl-PL" );
#else
std::locale pl( "pl_PL.UTF-8" );
#endif
constexpr auto v1 = 1234567;
constexpr auto v2 = 1234567.5;
std::println( "{}", v1 ); std::println( "{:L}", v1 ); std::cout << std::format( pl, "{:L}", v1 ) << '\n'; std::cout << std::format( pl, "{:.1Lf}", v2 ) << '\n'; std::cout << std::fixed << std::setprecision( 1 );
std::cout << v1 << " and " << v2 << '\n'; std::cout.imbue( pl );
std::cout << v1 << " and " << v2 << '\n'; }
Globalne locale programu to domyślnie "C", więc same {} i {:L} niczego nie zmieniają. Dopiero podanie pl razem z L daje polski zapis. Strumień po imbue robi to od razu, dla każdej liczby.
Jeśli chcemy mieć ten sam zapis niezależnie od zainstalowanych locale, możemy zdefiniować własne zasady formatowania liczb:
#include <format>
#include <locale>
#include <print>
#include <string>
struct polish_numbers
: std::numpunct < char >
{
char do_thousands_sep() const override { return ' '; }
std::string do_grouping() const override { return "\3"; }
char do_decimal_point() const override { return ','; }
};
int main()
{
std::locale pl( std::locale::classic(), new polish_numbers );
std::println( "{:L}", 1234567 ); std::println( "{}", std::format( pl, "{:L}", 1234567 ) ); std::println( "{}", std::format( pl, "{:.2Lf}", 1234.5 ) ); std::println( "{}", std::format( pl, "{}", 1234567 ) ); }
Obiekt utworzony przez
new nie wymaga ręcznego
delete:
std::locale przejmuje go na własność i sam usuwa, gdy zniknie ostatnia kopia tego locale (fasety są zliczane referencjami).
Napisy i znaki
Precyzja dla napisów
Precyzja ma sens nie tylko dla liczb. Dla napisów oznacza maksymalną liczbę wypisanych znaków, czyli napis jest obcinany (w strumieniach trzeba było do tego użyć
substr, a
printf ma
%.3s). Można ją łączyć z szerokością i wyrównaniem:
#include <print>
int main()
{
std::println( "|{:.3}|", "abcdef" ); std::println( "|{:8.3}|", "abcdef" ); std::println( "|{:>8.3}|", "abcdef" ); }
Znaki: typy c oraz d
Typ
char jest domyślnie wypisywany jako znak, ale można to zmienić literką typu:
d (albo
x,
o,
b) pokazuje kod znaku jako liczbę, a w drugą stronę
c wypisuje liczbę jako znak. Typy
signed char i
unsigned char są natomiast traktowane jak zwykłe liczby całkowite, a nie jak znaki:
#include <print>
int main()
{
std::println( "{} {:d} {:x}", 'A', 'A', 'A' ); std::println( "{:c} {:c}", 65, 0x41 ); std::println( "{} {}", static_cast < unsigned char >( 'A' ), static_cast < signed char >( 'A' ) ); }
Tryb debugowy: typ ?
Od
C++23 literka
? wypisuje napis albo znak w postaci "debugowej": w cudzysłowach (znak w apostrofach) i z zamienionymi na sekwencje ucieczki znakami specjalnymi, takimi jak
\n,
\t czy
\". Dzięki temu w logach widać dokładnie, co jest w napisie, np. niewidoczny znak końca linii. Starsze wersje bibliotek standardowych mogą jeszcze tego nie obsługiwać.
#include <print>
#include <string>
int main()
{
std::println( "{:?} {:?}", "a\tb\"c", '\n' ); std::println( "{:?}", std::string( "zażółć\n" ) ); }
Szerokość w Unicode
Szerokość pola jest liczona w kolumnach ekranu, a nie w bajtach:
std::format przyjmuje, że większość znaków zajmuje jedną kolumnę, a niektóre (m.in. znaki chińskie i emoji) dwie. Dlatego polskie litery, które w UTF-8 zajmują po dwa bajty, nie psują wyrównania - w przeciwieństwie do
printf
, który liczy bajty (dla
"%6s" i napisu
żółć nie dopisze żadnych spacji). Działa to wtedy, gdy kompilator traktuje literały napisów jako UTF-8 (w Visual Studio trzeba włączyć opcję
/utf-8).
#include <print>
int main()
{
std::println( "|{:6}|", "abcd" ); std::println( "|{:6}|", "żółć" ); std::println( "|{:6}|", "日本" ); }
Wskaźniki
Wskaźniki wypisujemy jako adres szesnastkowy z prefiksem
0x, ale tylko typu
void* (lub
const void*) oraz
nullptr. Wskaźnik na
char jest traktowany jak napis, a na każdy inny typ (np.
int*) trzeba najpierw rzutować na
const void*, inaczej będzie błąd kompilacji. Jedyną literką typu dla wskaźników jest
p (można ją pominąć), a
C++26 dodaje jeszcze wielkie
P (adres wielkimi literami) i flagę
0 (dopełnianie zerami). Adres pusty to
0x0, podczas gdy
printf
z
%p wypisuje w takiej sytuacji zależnie od implementacji, np.
(nil) w bibliotece glibc.
#include <print>
int main()
{
int x = 0;
int * ptr = & x;
const char * text = "tekst";
std::println( "{}", nullptr ); std::println( "{}", static_cast < void * >( nullptr ) ); std::println( "{}", static_cast < const void * >( ptr ) ); std::println( "{}", text ); std::println( "{}", static_cast < const void * >( text ) ); }
Strumienie są pod tym względem mniej restrykcyjne: wskaźnik na dowolny typ (np.
int*) jest po prostu zamieniany na adres, bez rzutowania. Zapis pustego wskaźnika zależy natomiast od implementacji, a
std::format zawsze daje
0x0:
#include <iostream>
int main()
{
int x = 0;
int * ptr = & x;
const char * text = "tekst";
std::cout << ptr << '\n'; std::cout << text << '\n'; std::cout << static_cast < const void * >( text ) << '\n'; std::cout << nullptr << '\n'; std::cout << static_cast < void * >( nullptr ) << '\n'; }
Czas i daty
Jedną z największych zalet
std::format w porównaniu ze strumieniami jest formatowanie typów z
std::chrono (od
C++20). Datę, godzinę i czas trwania można wypisać wprost, a jeśli chcemy własny wygląd, po dwukropku podajemy napis w stylu
strftime, złożony ze znaków poprzedzonych
%:
Dla czasu trwania wypisywana jest też jednostka. Nazwy dni i miesięcy są domyślnie angielskie - polskie dostaniemy, gdy przed specyfikacją dodamy
L (np.
{:L%A}) i podamy odpowiednie locale.
#include <chrono>
#include <print>
int main()
{
using namespace std::chrono;
sys_days battle = year { 1683 } / September / 12;
std::println( "{}", battle ); std::println( "{:%d.%m.%Y}", battle ); std::println( "{:%A}", battle ); seconds elapsed { 3725 };
std::println( "{}", elapsed ); std::println( "{:%H:%M:%S}", elapsed ); std::println( "{}", 1500ms ); }
Polskie nazwy dni i miesięcy dostajemy po podaniu locale (jak w rozdziale o ustawieniach lokalnych) i literki
L przed specyfikacją. Bez
L locale jest ignorowane, więc ostatnia linia nadal wypisze angielską nazwę. Dokładna forma nazw (np.
września, a nie
wrzesień) zależy od systemu:
#include <chrono>
#include <format>
#include <locale>
#include <print>
int main()
{
#ifdef _WIN32
std::locale pl( "pl-PL" );
#else
std::locale pl( "pl_PL.UTF-8" );
#endif
using namespace std::chrono;
sys_days battle = year { 1683 } / September / 12;
std::println( "{}", std::format( pl, "{:L%A, %d %B %Y}", battle ) ); std::println( "{}", std::format( pl, "{:L%a %d %b}", battle ) ); std::println( "{}", std::format( pl, "{:%A}", battle ) ); }
Kontenery, krotki i inne zakresy
W
C++23 std::format i
std::print nauczyły się wypisywać też kontenery, pary, krotki i ogólnie zakresy (ranges), bez pisania pętli. Elementy są w nawiasach kwadratowych, napisy i znaki w trybie debugowym (z cudzysłowami), mapy w klamrach, a pary i krotki w nawiasach okrągłych. Specyfikację po dwukropku można zastosować do całego zakresu, a po drugim dwukropku - do każdego elementu osobno:
{:n} usuwa nawiasy, a
{::02} czy
{::x} formatuje każdy element. Starsze wersje bibliotek standardowych mogą jeszcze tego nie mieć (alternatywą jest biblioteka fmt, opisana niżej).
#include <map>
#include <print>
#include <string>
#include <tuple>
#include <vector>
int main()
{
std::vector < int > v { 1, 2, 3 };
std::println( "{}", v ); std::println( "{:n}", v ); std::println( "{::02}", v ); std::println( "{::x}", std::vector < int > { 10, 11, 255 } ); std::println( "{}", std::vector < std::string > { "a", "b c" } ); std::println( "{}", std::map < int, std::string > { { 1, "x" }, { 2, "y" } } ); std::println( "{}", std::pair { 1, 'a' } ); std::println( "{}", std::tuple { 1, 2.5, "t" } ); }
Strumienie nie mają takiej obsługi, więc podobny efekt trzeba osiągnąć pętlą. Wersja z
std::ostream_iterator i
std::ranges::copy jest krótsza, ale dopisuje separator także po ostatnim elemencie. Formatowanie elementów (odpowiednik
{::02}) wymaga ustawiania manipulatorów w pętli, a dla map i krotek trzeba napisać własny kod:
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <vector>
int main()
{
std::vector < int > v { 1, 2, 3 };
std::cout << '[';
for( std::size_t i = 0; i < v.size(); ++i )
std::cout <<( i ? ", "
: "" ) << v[ i ];
std::cout << "]\n"; std::ranges::copy( v, std::ostream_iterator < int >( std::cout, ", " ) ); std::cout << '\n';
std::cout << '[' << std::setfill( '0' );
for( std::size_t i = 0; i < v.size(); ++i )
std::cout <<( i ? ", "
: "" ) << std::setw( 2 ) << v[ i ];
std::cout << "]\n"; }
Skąd wiadomo, jak wypisać zakres: format_kind
Skąd
std::format wie, że
std::vector ma wypisać w nawiasach kwadratowych, a
std::set i
std::map w klamrach? Decyduje o tym zmienna szablonowa
std::format_kind (C++23). Dla każdego zakresu przechowuje ona wartość z wyliczenia
std::range_format, która mówi, jak zakres wypisać:
Biblioteka wybiera wartość sama: zakres, którego elementy mają ten sam typ co on sam (jak
std::filesystem::path), jest
disabled, zakres z kluczami i wartościami jest
map, z samymi kluczami
set, a cała reszta to
sequence. Dla własnego typu możemy tę decyzję zmienić, specjalizując
format_kind. Poniższy przykład pokazuje, jak biblioteka klasyfikuje typy standardowe, a potem dwie takie same klasy: jedna zostaje domyślnie sekwencją, druga jest zamieniona na zbiór:
#include <format>
#include <map>
#include <print>
#include <set>
#include <string_view>
#include <vector>
constexpr std::string_view names[ ] = { "disabled", "map", "set", "sequence", "string", "debug_string" };
struct NumberList
{
std::vector < int > data;
auto begin() const { return data.begin(); }
auto end() const { return data.end(); }
};
struct NumberSet
{
std::vector < int > data;
auto begin() const { return data.begin(); }
auto end() const { return data.end(); }
};
template < >
inline constexpr std::range_format std::format_kind < NumberSet > = std::range_format::set;
int main()
{
std::println( "{}", names[ static_cast < int >( std::format_kind < std::vector < int >> ) ] ); std::println( "{}", names[ static_cast < int >( std::format_kind < std::set < int >> ) ] ); std::println( "{}", names[ static_cast < int >( std::format_kind < std::map < int, int >> ) ] ); std::println( "{}", names[ static_cast < int >( std::format_kind < NumberList > ) ] ); std::println( "{}", NumberList { { 1, 2, 3 } } ); std::println( "{}", NumberSet { { 1, 2, 3 } } ); }
Argumenty pozycyjne
Domyślnie argumenty podstawiane są w kolejności, w jakiej je podano. Wewnątrz klamer możemy jednak wskazać numer argumentu (liczony od zera) - wtedy można je powtarzać, pomijać i zamieniać kolejnością. Przydaje się to np. przy tłumaczeniu programu na inny język, gdy szyk zdania jest inny i jedna kolejność argumentów nie pasuje do wszystkich wersji. Powtarzanie argumentu pozwala też pokazać tę samą liczbę w różnych systemach liczbowych (linia z
{0:d} {0:o} {0:b} {0:x}).
#include <print>
int main()
{
std::println( "{} {}", "a", "b" ); std::println( "{1} {0}", "a", "b" ); std::println( "{0} {0} {0}", "hej" ); std::println( "{0} {2}", "a", "b", "c" ); std::println( "|{:>{}}|", "x", 5 ); std::println( "|{0:>{1}}|", "x", 5 ); std::println( "|{0:{1}.{2}f}|", 3.14159, 10, 2 ); std::println( "{0:d} {0:o} {0:b} {0:x}", 42 ); std::println( "{{}} {{{}}}", 42 ); }
Zasady:
Funkcja
printf w wersji POSIXowej obsługuje argumenty pozycyjne przez:
%1$s.
Gdzie trafia wynik
Do napisu lub bufora: format_to, format_to_n, formatted_size
std::format za każdym razem tworzy nowy
std::string, czyli alokuje pamięć. Gdy formatujemy dużo razy albo chcemy dopisywać do istniejącego napisu czy tablicy, są trzy pomocnicze funkcje:
#include <format>
#include <iterator>
#include <print>
#include <string>
int main()
{
std::string result;
std::format_to( std::back_inserter( result ), "{}-{}", 1, 2 );
std::format_to( std::back_inserter( result ), "|{:>4}", 3 );
std::println( "{}", result ); char buffer[ 16 ];
auto res = std::format_to_n( buffer, sizeof( buffer ) - 1, "{:.3f} i dluga reszta", 3.14159 );
* res.out = '\0';
std::println( "{} (pelny wynik: {} znakow)", buffer, res.size ); std::println( "{}", std::formatted_size( "{:>10}", 1 ) ); }
Na ekran i do plików: print i println
std::print i
std::println domyślnie piszą na standardowe wyjście, ale pierwszym argumentem można wskazać inne miejsce: wskaźnik
FILE* (np.
stderr albo plik z
fopen) lub dowolny strumień
C++ (
std::cout,
std::ofstream).
println sam dodaje znak nowej linii (zwykły
'\n', a nie
std::endl, więc nie wymusza opróżnienia bufora), a wywołany bez argumentów wypisuje samą pustą linię (ta możliwość jest formalnie dopiero w
C++26).
#include <fstream>
#include <iostream>
#include <print>
int main()
{
std::print( "Bez nowej linii, " );
std::println( "a tu z nową linią: {}", 1 );
std::println( stderr, "To idzie na stderr: {}", 2 );
std::print( std::cout, "To idzie do cout: {}\n", 3 );
std::println(); std::ofstream file( "wynik.txt" );
std::println( file, "To idzie do pliku: {}", 4 );
}
std::print bez pierwszego argumentu pisze przez C-owe
stdout, tak samo jak
printf
. Jeśli w programie wyłączymy synchronizację strumieni (
std::ios::sync_with_stdio(false)) i wymieszamy go z
std::cout, kolejność wydruku może się zmienić. Dodatkowo
std::print jest zaprojektowane tak, aby na terminalu (także w konsoli Windows) poprawnie wypisywać tekst w UTF-8.
Jeszcze jedna różnica względem strumieni, na którą zwraca uwagę autor propozycji
std::print w
swoim wpisie o std::print: tak jak w
printf
, fragmenty jednego komunikatu wypisywanego z wielu wątków nie mieszają się z komunikatami z innych wątków, a przy
std::cout mogą się przeplatać (chyba że użyjemy
std::osyncstream z
C++20). Pisze tam też, że formatowanie w
std::print jest domyślnie niezależne od locale, co wpływa zarówno na niezawodność, jak i na szybkość.
Sprawdzanie formatu w czasie kompilacji
Duża różnica względem
printf
:
std::format sprawdza napis formatujący
w czasie kompilacji. Błędna specyfikacja albo niezgodna liczba argumentów to błąd kompilacji, a nie niespodzianka w czasie działania programu:
#include <format>
#include <string>
int main()
{
std::string s1 = std::format( "{:d}", "tekst" ); std::string s2 = std::format( "{} {}", 1 ); }
W GCC 14 komunikat zaczyna się od
call to consteval function 'std::basic_format_string<...>' is not a constant expression
i dopiero niżej jest właściwy powód (treść zależy od kompilatora).
Co jest błędem kompilacji?
Specyfikacja formatu musi pasować do typu argumentu. Kilka reguł pozwala przewidzieć, kiedy kompilator ją odrzuci:
Pozostałe zestawienia są poprawne, np.
{:>5} czy
{:#} dla
int,
{:5.2} dla napisu albo
{:d} i
{:s} dla
bool.
Gdy format nie jest znany w czasie kompilacji?
Gdy napis formatujący nie jest znany w czasie kompilacji (np. wczytujemy go z pliku), używamy
std::vformat razem z
std::make_format_args. Sprawdzenie odbywa się wtedy w czasie działania programu i błąd zgłaszany jest wyjątkiem
std::format_error. Uwaga:
make_format_args przyjmuje zmienne (lvalue), nie tymczasowe wartości.
#include <format>
#include <print>
#include <string>
int main()
{
std::string format_str = "{:>8}|";
int n = 42;
std::println( "{}", std::vformat( format_str, std::make_format_args( n ) ) );
try
{
std::string bad_format = "{:d}";
const char * text = "abc";
std::println( "{}", std::vformat( bad_format, std::make_format_args( text ) ) );
}
catch( const std::format_error & e )
{
std::println( "format_error: {}", e.what() );
}
}
Wydruk (treść komunikatu wyjątku zależy od implementacji):
42|
format_error: format error: failed to parse format-spec
vformat czy dynamic_format?
W
C++26 pojawił się
std::dynamic_format (w starszych implementacjach nosi jeszcze nazwę
std::runtime_format), a w bibliotece fmt odpowiada temu
fmt::runtime. Oba narzędzia służą do użycia napisu formatującego, który jest znany dopiero w czasie działania programu. Różnią się sposobem użycia:
Jeśli mamy
C++26 i po prostu wczytaliśmy napis formatujący z pliku czy z tłumaczenia, wygodniejszy jest
dynamic_format.
vformat jest potrzebny, gdy piszemy funkcję opakowującą (np. do logowania), która dostaje napis i argumenty od wywołującego i przekazuje je dalej, a także w standardach starszych niż
C++26. Poniższy przykład pokazuje oba sposoby oraz funkcję opakowującą, która dzięki
std::format_string zachowuje sprawdzanie napisu w czasie kompilacji:
#include <format>
#include <print>
#include <string>
template < typename ... Args >
void log( std::format_string < Args ... > fmt, Args && ... args )
{
std::println( "[LOG] {}", std::vformat( fmt.get(), std::make_format_args( args ... ) ) );
}
int main()
{
std::string format_str = "{:>6}|{}";
int a = 42;
std::println( "{}", std::vformat( format_str, std::make_format_args( a, "x" ) ) ); std::println( "{}", std::format( std::dynamic_format( format_str ), 42, "x" ) ); std::println( std::dynamic_format( format_str ), 7, "y" ); log( "{} + {} = {}", 2, 3, 5 ); }
Co jest w środku format_args: visit_format_arg
Gdy
std::vformat dostaje
std::format_args, argumenty są "wymazane z typów": każdy z nich jest zapamiętany w obiekcie
std::basic_format_arg, który w środku zachowuje się jak
std::variant. Może przechowywać tylko ograniczoną liczbę typów:
bool,
char,
int,
unsigned,
long long,
unsigned long long,
float,
double,
long double,
const char*,
std::string_view,
const void*, specjalny uchwyt
handle dla wszystkich pozostałych (np. własnych) typów oraz
std::monostate dla pustego argumentu. Typy spoza tej listy są zamieniane na najbliższy z niej:
short staje się
int, a
std::string -
std::string_view.
Funkcja
std::visit_format_arg działa jak
std::visit dla tego wariantu: wywołuje podany obiekt funkcyjny z wartością, którą faktycznie przechowuje argument. W zwykłych programach jest rzadko potrzebna - przydaje się przy własnej implementacji
vformat, w funkcjach logujących, które chcą traktować różne typy inaczej, albo w narzędziach sprawdzających argumenty. Poniższy program wypisuje typ i wartość każdego argumentu przekazanego przez
format_args (wywołanie
args.get(i) zwraca pusty argument, gdy
i jest poza zakresem, a
if (!arg) to sprawdza):
#include <format>
#include <print>
#include <string>
#include <string_view>
#include <type_traits>
void describe( std::format_args args )
{
for( std::size_t i = 0;; ++i )
{
auto arg = args.get( i );
if( !arg )
break;
std::visit_format_arg([ i ]( auto && value )
{
using T = std::decay_t < decltype( value ) >;
if constexpr( std::is_same_v < T, int > )
std::println( "{}: int = {}", i, value );
else if constexpr( std::is_same_v < T, double > )
std::println( "{}: double = {}", i, value );
else if constexpr( std::is_same_v < T, bool > )
std::println( "{}: bool = {}", i, value );
else if constexpr( std::is_same_v < T, std::string_view > )
std::println( "{}: napis = \"{}\"", i, value );
else
std::println( "{}: inny typ", i );
}, arg );
}
}
int main()
{
int a = 42;
double b = 3.14;
std::string s = "tekst";
bool f = true;
short sh = 7;
describe( std::make_format_args( a, b, s, f, sh ) );
}
Wynik:
0: int = 42
1: double = 3.14
2: napis = "tekst"
3: bool = true
4: int = 7
Widać, że
std::string dotarł jako napis (
string_view), a
short jako
int.
W
C++26 std::visit_format_arg jest
przestarzałe (deprecated) na rzecz funkcji składowej
arg.visit(...) (propozycja
P2637R3). Wystarczy więc w powyższym przykładzie zamienić
std::visit_format_arg((auto&& value) { ... }, arg); na
arg.visit((auto&& value) { ... });. Nowa wersja pozwala dodatkowo podać typ zwracany, np.
arg.visit<void>(...). Kompilatory z nowszymi bibliotekami standardowymi ostrzegają już przed użyciem starej funkcji, a starsze jeszcze nie mają funkcji składowej.
Własny formatter
Typy wbudowane i standardowe są już obsłużone, ale własny typ trzeba nauczyć formatowania samemu. Robimy to specjalizując szablon
std::formatter (w bibliotece fmt jest to
fmt::formatter o takim samym interfejsie). Specjalizacja potrzebuje dwóch metod:
Najprostszy przykład nie obsługuje żadnych opcji (po dwukropku nic nie można podać):
#include <format>
#include <print>
struct Point
{
double x;
double y;
};
template < >
struct std::formatter < Point >
{
constexpr auto parse( std::format_parse_context & ctx )
{
return ctx.begin();
}
auto format( const Point & p, std::format_context & ctx ) const
{
return std::format_to( ctx.out(), "({}, {})", p.x, p.y );
}
};
int main()
{
Point p { 1.5, - 2 };
std::println( "{}", p ); }
Taki formatter odrzuci więc
{:>12} błędem kompilacji. Jeżeli chcemy obsłużyć szerokość i wyrównanie, najłatwiej odziedziczyć po formatterze typu, który już to potrafi, np.
std::formatter<std::string_view>, i przekazać mu gotowy tekst. Poniżej przykład dla
enum class, którego wartości chcemy wypisywać jako nazwy:
#include <format>
#include <print>
#include <string_view>
enum class Color { red, green, blue };
template < >
struct std::formatter < Color >
: std::formatter < std::string_view >
{
auto format( Color k, std::format_context & ctx ) const
{
std::string_view name = "nieznany";
switch( k )
{
case Color::red: name = "czerwony"; break;
case Color::green: name = "zielony"; break;
case Color::blue: name = "niebieski"; break;
}
return std::formatter < std::string_view >::format( name, ctx );
}
};
int main()
{
std::println( "|{}|", Color::green ); std::println( "|{:>12}|", Color::blue ); std::println( "|{:.^12}|", Color::red ); }
Możemy też oddać obsługę opcji formatterowi liczby. Wtedy specyfikacja użyta dla naszego typu zostaje zastosowana do każdego z pól (tu: obu współrzędnych punktu):
#include <format>
#include <print>
struct Point
{
double x;
double y;
};
template < >
struct std::formatter < Point >
{
std::formatter < double > number;
constexpr auto parse( std::format_parse_context & ctx )
{
return number.parse( ctx );
}
auto format( const Point & p, std::format_context & ctx ) const
{
auto out = ctx.out();
* out++ = '(';
ctx.advance_to( out );
out = number.format( p.x, ctx );
out = std::format_to( out, ", " );
ctx.advance_to( out );
out = number.format( p.y, ctx );
* out++ = ')';
return out;
}
};
int main()
{
Point p { 1.5, - 2 };
std::println( "{}", p ); std::println( "{:.2f}", p ); std::println( "{:+8.1e}", p ); }
Czego std::format nie sformatuje domyślnie
Nie każdy typ ma formatter, a próba wypisania takiego typu to błąd kompilacji. Najczęstsze przypadki:
Przejście z printf na std::format
Kto zna
printf
, zwykle szybko odnajdzie się w
std::format. Poniższa tabela zbiera najczęstsze specyfikacje (wszystkie pary dają identyczny wynik):
Najważniejsza pułapka dotyczy wyrównania napisów:
#include <cstdio>
#include <print>
int main()
{
std::printf( "|%10s|\n", "tekst" ); std::println( "|{:10}|", "tekst" ); std::println( "|{:>10}|", "tekst" ); }
Jeśli w projekcie jest dużo kodu z
printf
, a przepisywanie wszystkich napisów formatujących byłoby żmudne, biblioteka fmt ma gotowy zamiennik:
fmt::printf i
fmt::fprintf. Zachowują składnię
printf
, więc często wystarczy zmienić samą nazwę funkcji, a w zamian dostajemy sprawdzanie typów (niezgodność to wyjątek
fmt::format_error, a nie zachowanie niezdefiniowane), możliwość podania
std::string do
%s bez
c_str() oraz mniejszy narzut - w teście szybkości na końcu artykułu
fmt::printf był około dwa razy szybszy od
printf
. Szczegóły są w rozdziale o bibliotece fmt.
Zamiast std::to_string
Do zamiany liczby na napis często używa się
std::to_string. Dla liczb całkowitych to dobre rozwiązanie, ale dla zmiennoprzecinkowych działa jak
%f z
printf
: zawsze wypisuje sześć cyfr po przecinku i nie da się tego zmienić. Mała liczba może się wtedy zamienić na samo zero, a zwykła dostaje zbędne zera na końcu.
std::format można użyć w każdym miejscu, w którym był
std::to_string, z pełną kontrolą nad zapisem:
#include <format>
#include <print>
#include <string>
int main()
{
std::println( "{}", std::to_string( 3.14 ) ); std::println( "{}", std::to_string( 1e-7 ) ); std::println( "{}", std::to_string( 1.0 / 3.0 ) ); std::println( "{}", std::format( "{}", 3.14 ) ); std::println( "{}", std::format( "{}", 1e-7 ) ); std::println( "{}", std::format( "{:.2f}", 3.14 ) ); }
Biblioteka fmt
Skąd wziąć bibliotekę
Biblioteka {fmt} ma stronę
fmt.dev z dokumentacją, a jej kod źródłowy jest w repozytorium
fmtlib/fmt na GitHubie (licencja MIT). Jej autorem jest
Victor Zverovich, który napisał też propozycję
std::print do standardu. Najwygodniej zainstalować ją menedżerem pakietów:
Wersja z repozytoriów dystrybucji bywa dużo starsza od aktualnej - w
Ubuntu 24.04 jest to fmt 9.1, a najnowsze wydanie w chwili pisania artykułu to 12.2. Na starej wersji nie zadziała część przykładów z tego artykułu (np. formatowanie
std::optional przez
fmt/std.h).
O instalowaniu bibliotek przez vcpkg i używaniu ich razem z CMake pisałem w
artykule o menedżerze pakietów vcpkg i CMake.
Jak zbudować i podłączyć do projektu
W przykładach poniżej program jest zawsze taki sam:
#include <fmt/core.h>
int main()
{
fmt::print( "Witaj, {}!\n", "fmt" );
}
Tylko nagłówki
Najprościej pobrać źródła i włączyć tryb header-only makrem
FMT_HEADER_ONLY:
git clone --branch 12.2.0 --depth 1 https://github.com/fmtlib/fmt.git
g++ -std=c++23 -DFMT_HEADER_ONLY -Ifmt/include main.cpp -o main
W CMake odpowiada temu cel
fmt::fmt-header-only. Tryb header-only wydłuża kompilację każdego pliku dołączającego nagłówki fmt, dlatego dokumentacja zaleca wersję skompilowaną. Nie działa w nim też nagłówek
fmt/os.h, który wymaga skompilowanej biblioteki.
Skompilowana biblioteka - budowanie ze źródeł
git clone --branch 12.2.0 --depth 1 https://github.com/fmtlib/fmt.git
cd fmt
cmake -B build -DCMAKE_BUILD_TYPE=Release -DFMT_TEST=OFF -DFMT_DOC=OFF
cmake --build build
cmake --install build --prefix $HOME/fmt-install
Teraz w projekcie wystarczy
CMakeLists.txt:
cmake_minimum_required(VERSION 3.14)
project(demo CXX)
find_package(fmt CONFIG REQUIRED)
add_executable(main main.cpp)
target_link_libraries(main PRIVATE fmt::fmt)
oraz wskazanie, gdzie zainstalowaliśmy bibliotekę:
cmake -B build -DCMAKE_PREFIX_PATH=$HOME/fmt-install
cmake --build build
Bez CMake wystarczy dopisać katalogi i bibliotekę do polecenia kompilatora:
g++ -std=c++23 -I$HOME/fmt-install/include main.cpp -L$HOME/fmt-install/lib -lfmt -o main
.
Biblioteka z menedżera pakietów
Po
sudo apt install libfmt-dev
ten sam
CMakeLists.txt działa bez żadnych dodatkowych opcji, a bez CMake kompilujemy przez
g++ -std=c++23 main.cpp -lfmt -o main
. W przypadku vcpkg (po
vcpkg install fmt
) podajemy CMake plik konfiguracyjny vcpkg:
cmake -B build -DCMAKE_TOOLCHAIN_FILE=[ścieżka do vcpkg]/scripts/buildsystems/vcpkg.cmake
cmake --build build
Pobranie przez CMake (FetchContent)
CMake może też sam pobrać i zbudować fmt podczas konfiguracji projektu, wtedy nic nie trzeba instalować:
cmake_minimum_required(VERSION 3.14)
project(demo CXX)
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt
GIT_TAG 12.2.0)
FetchContent_MakeAvailable(fmt)
add_executable(main main.cpp)
target_link_libraries(main PRIVATE fmt::fmt)
Jeszcze inną możliwością jest wrzucenie źródeł fmt do katalogu projektu i użycie
add_subdirectory(fmt) w miejsce
find_package.
Wymagania i wersje
Biblioteka fmt działa już od
C++11. Kod z przykładów
fmt::print,
fmt::arg,
fmt::join, zakresów czy
FMT_COMPILE kompiluje się w GCC z opcjami
-std=c++11,
c++14,
c++17 i
c++20. Dlatego to ona jest rozwiązaniem dla projektów, które utknęły na starszym
C++. Przykłady w dalszej części artykułu były kompilowane z fmt 12.2.
fmt rozwija się szybciej niż standard
Standardowe std::format powstało z inspiracji fmt, ale rozwój biblioteki na tym się nie skończył. Kolejne elementy trafiały najpierw do fmt, a dopiero po latach - w zmienionej postaci - do standardu: std::print z C++23 odpowiada fmt::print (mają tego samego autora), a formatowanie zakresów i krotek z C++23 powstało z fmt jako punktem odniesienia (propozycja do standardu pokazuje jako przykład fmt/ranges.h).
Standard zmienia się co trzy lata, a nowa biblioteka standardowa trafia do nas razem z nowym kompilatorem. Bibliotekę fmt można zaktualizować w dowolnym momencie, niezależnie od kompilatora, i używać z kompilatorami, które nie mają jeszcze std::format czy std::print. Dlatego lista tego, co jest w fmt, a czego nie ma w standardzie, jest dłuższa niż lista odwrotna, ale może się z czasem skracać, w miarę jak kolejne rzeczy przechodzą do standardu.
Unicode w fmt
Dokumentacja fmt chwali się przenośną obsługą Unicode: napisy typu
char są traktowane jako UTF-8 i mają się poprawnie wypisywać na Linuksie, macOS, a nawet w konsoli Windows, niezależnie od stron kodowych. Szerokość i precyzja są liczone w znakach (kolumnach), a nie w bajtach, więc polskie litery nie psują wyrównania, a precyzja nie rozetnie litery na pół. Tak samo działa tryb debugowy
?. Domyślnie formatowanie jest niezależne od locale - można je włączyć literką
L, tak jak w standardzie. Tylko w Windows/MSVC można Unicode wyłączyć opcją CMake
FMT_UNICODE=OFF:
#include <fmt/format.h>
int main()
{
fmt::print( "Zażółć gęślą jaźń!\n" ); fmt::print( "|{:>16}|\n", "Zażółć gęślą" ); fmt::print( "|{:^12}|\n", "żółć" ); fmt::print( "|{:.4}|\n", "Zażółć gęślą jaźń" ); fmt::print( "{:?}\n", "zażółć\n" ); }
Co ma fmt::format czego nie ma std::format
Argumenty nazwane, zakresy i łączenie elementów
W standardowym
std::format argumenty są tylko pozycyjne (
{0},
{1}). W fmt można je nazwać. Bez dodatkowego wysiłku sformatujemy też kontenery i krotki, a
fmt::join pozwala wypisać elementy z własnym separatorem.
fmt::group_digits wstawia separatory tysięcy niezależnie od locale. Argument nazwany można podać funkcją
fmt::arg("imie", "Ala") albo krócej literałem
_a z przestrzeni nazw
fmt::literals (po
using namespace fmt::literals;), np.
"x"_a = 7 - oba zapisy robią to samo:
#include <fmt/format.h>
#include <fmt/ranges.h>
#include <map>
#include <string>
#include <tuple>
#include <vector>
int main()
{
using namespace fmt::literals;
fmt::print( "{imie} ma {wiek} lat\n", fmt::arg( "imie", "Ala" ), fmt::arg( "wiek", 20 ) ); fmt::print( "{x}+{x}\n", "x"_a = 7 ); std::vector < int > v { 1, 2, 3 };
fmt::print( "{}\n", v ); fmt::print( "{}\n", std::map < std::string, int > { { "a", 1 }, { "b", 2 } } ); fmt::print( "{}\n", std::tuple < int, char, std::string > { 1, 'a', "x" } ); fmt::print( "{}\n", fmt::join( v, "; " ) ); fmt::print( "{::02}\n", v ); fmt::print( "{}\n", fmt::group_digits( 1234567 ) ); }
Formatowanie kontenerów i krotek jest w standardzie dopiero od
C++23 (opisaliśmy je wyżej, w rozdziale o zakresach; wymaga nowej biblioteki standardowej - GCC 14 jeszcze tego nie obsługuje), a fmt daje to od
C++11.
Typy ze standardowej biblioteki
Nagłówek
fmt/std.h obsługuje formatowanie kolejnych typów standardowych. Na liście standardowych formatterów na
cppreference nie ma
std::optional ani
std::variant, które obsługuje biblioteka fmt:
#include <fmt/std.h>
#include <filesystem>
#include <optional>
#include <variant>
int main()
{
fmt::print( "{}\n", std::optional < int >( 3 ) ); fmt::print( "{}\n", std::variant < int, double >( 2.5 ) ); fmt::print( "{}\n", std::filesystem::path( "/tmp/a.txt" ) ); }
Podobnie
std::filesystem::path -
formatter dla niego trafił do standardu dopiero w
C++26 (obsługuje m.in.
? oraz
g, czyli zapis ze znakami
/ niezależnie od systemu), więc starsze wersje bibliotek standardowych go nie mają.
Kolory i zapis do pliku
fmt/color.h dodaje kolory i style terminala (kody ANSI), a
fmt/os.h - wygodne zapisywanie do pliku bez strumieni:
#include <fmt/color.h>
#include <fmt/os.h>
int main()
{
fmt::print( fg( fmt::color::crimson ) | fmt::emphasis::bold, "czerwony, pogrubiony tekst\n" );
fmt::print( "{}\n", fmt::styled( "pomarańczowy", fmt::fg( fmt::rgb( 255, 128, 0 ) ) ) );
auto f = fmt::output_file( "wynik.txt" );
f.print( "Liczba: {}\n", 42 );
}
Pierwsze wywołanie wypisze czerwony, pogrubiony tekst, drugie pomarańczowy, a plik
wynik.txt będzie zawierał linię
Liczba: 42.
Obiekt zwracany przez
fmt::output_file to plik z własnym buforem, który nie jest synchronizowany między wątkami (stąd w README: "zapis z jednego wątku") - nie ma więc kosztu blokowania, który ponosi
fprintf
. Według autora biblioteki, który opisał to w
osobnym wpisie o rozmiarze bufora, może to być od 5 do 9 razy szybsze niż
fprintf
. Domyślnie bufor jest większy niż w
stdio, a jego rozmiar można zmienić:
fmt::output_file("wynik.txt", fmt::buffer_size = 1 << 16). Plik zamyka się sam w destruktorze, albo wcześniej przez
close(). Do tego samego pliku z kilku wątków lepiej użyć zwykłego, synchronizowanego mechanizmu.
Zapis do bufora: memory_buffer, format_to, format_int
Funkcje
format_to,
format_to_n i
formatted_size działają tak samo jak w standardzie (opisaliśmy je w rozdziale o miejscu zapisu wyniku). Dokumentacja fmt pokazuje przy nich przykład z wektorem znaków jako buforem. Biblioteka ma też własny bufor
fmt::memory_buffer, który pierwsze 500 znaków trzyma w samym obiekcie, więc krótkie wyniki nie alokują pamięci, oraz
fmt::format_int do bardzo szybkiego zamieniania liczby całkowitej na tekst:
#include <fmt/format.h>
#include <iterator>
#include <string>
#include <vector>
int main()
{
auto out = std::vector < char >();
fmt::format_to( std::back_inserter( out ), "{}", 42 );
fmt::print( "{}\n", std::string( out.begin(), out.end() ) ); fmt::memory_buffer buf;
fmt::format_to( std::back_inserter( buf ), "{}-{}", 1, 2 );
fmt::format_to( std::back_inserter( buf ), "|{:>4}", 3 );
fmt::print( "{}\n", fmt::to_string( buf ) ); fmt::print( "{} {}\n", buf.size(), buf.capacity() ); fmt::print( "{}\n", fmt::format_int( 1234567 ).c_str() ); }
Data i czas
Nagłówek
fmt/chrono.h daje formatowanie czasu trwania, punktów w czasie oraz
std::tm według tych samych specyfikacji w stylu
strftime, co w
std::format. Różnica jest taka, że standardowe
std::format nie umie formatować
std::tm (to typ z języka
C, którego używa
std::localtime), a fmt tak. Przykład działa już od
C++14 (używa literałów czasu, takich jak
42s), bo nie wymaga typów dat z
C++20:
#include <fmt/chrono.h>
#include <chrono>
#include <ctime>
int main()
{
using namespace std::literals::chrono_literals;
fmt::print( "{} {}\n", 42s, 100ms ); fmt::print( "{:%H:%M:%S}\n", 3h + 15min + 30s ); std::tm tm = fmt::gmtime( std::time_t { 0 } );
fmt::print( "{:%Y-%m-%d %H:%M:%S}\n", tm ); fmt::print( "{:%d.%m.%Y}\n", tm ); auto now = std::chrono::system_clock::now();
fmt::print( "Dzisiaj jest {:%d.%m.%Y}, godzina {:%H:%M}\n", now, now ); }
Do zamiany
time_t na
std::tm fmt udostępnia
fmt::gmtime. W standardzie (nagłówek
<ctime>) jest odpowiednik,
std::gmtime (i
std::localtime), ale zwraca on wskaźnik na wewnętrzny obiekt, wspólny dla całego programu i nadpisywany przy kolejnym wywołaniu, więc nie nadaje się do użycia z wielu wątków.
fmt::gmtime zwraca
std::tm przez wartość i według dokumentacji jest bezpieczny wątkowo na większości platform. Od
C++20 std::tm w ogóle nie jest potrzebny:
std::chrono::system_clock::from_time_t zamienia
time_t na typ z
std::chrono, który
std::format już potrafi wypisać.
Argumenty budowane w czasie działania, kompilacja formatu
fmt::dynamic_format_arg_store pozwala zbudować listę argumentów w pętli, czyli bez znajomości ich liczby w czasie kompilacji (w standardzie
make_format_args tego nie umożliwia). Makro
FMT_COMPILE parsuje napis formatujący już podczas kompilacji i zamienia go na kod formatujący, co według dokumentacji fmt daje wydajniejszy kod:
#include <fmt/args.h>
#include <fmt/compile.h>
#include <string>
int main()
{
fmt::dynamic_format_arg_store < fmt::format_context > store;
store.push_back( 1 );
store.push_back( "dwa" );
fmt::print( "{}\n", fmt::vformat( "{} {}", store ) ); std::string s = fmt::format( FMT_COMPILE( "{:>6}|" ), 42 );
fmt::print( "{}\n", s ); }
Przenoszenie starego kodu (z operatorów strumienia)
Dla własnych typów, które mają przeciążony
operator<< istnieje
fmt::streamed (z
fmt/ostream.h), który załatwia sprawę bez pisania formattera. Opakowany obiekt można też wyrównać jak zwykły napis:
#include <fmt/ostream.h>
#include <ostream>
struct OldType { int a; };
std::ostream & operator <<( std::ostream & os, const OldType & s )
{
return os << "Stary#" << s.a;
}
int main()
{
fmt::print( "{}\n", fmt::streamed( OldType { 7 } ) ); fmt::print( "|{:>12}|\n", fmt::streamed( OldType { 7 } ) ); }
Przenoszenie starego kodu (z printf)
fmt::printf,
fmt::fprintf i
fmt::sprintf (z
fmt/printf.h) to bezpieczne wersje funkcji z rodziny
printf
, przydatne gdy w projekcie jest dużo napisów w starym stylu. Używają składni
printf
(razem z POSIX-owymi argumentami pozycyjnymi, np.
%2$s), ale w odróżnieniu od oryginałów sprawdzają typy argumentów: przy niezgodności zgłaszają wyjątek
fmt::format_error zamiast zachowania niezdefiniowanego. Uwaga:
fmt::sprintf zwraca
std::string, więc nie ma takiej samej sygnatury jak
sprintf. Poza tym przyspieszają wypisywanie - patrz porównanie szybkości na końcu artykułu.
#include <fmt/printf.h>
#include <cstdio>
#include <string>
int main()
{
fmt::printf( "%d + %d = %d\n", 2, 3, 5 ); fmt::fprintf( stderr, "Blad: %s\n", "brak pliku" ); std::string s = fmt::sprintf( "%5.2f|%-4d|%s", 3.14159, 42, "x" );
fmt::printf( "%s\n", s ); fmt::printf( "%s|\n", std::string( "bez c_str()" ) ); fmt::printf( "%2$s %1$s\n", "a", "b" ); try {
fmt::printf( "%d\n", "tekst" );
} catch( const fmt::format_error & e ) {
fmt::printf( "wyjatek: %s\n", e.what() ); }
}
Co ma std::format czego nie ma fmt::format
Biblioteka fmt ma odpowiedniki niemal wszystkiego, co jest w standardzie, pod tymi samymi nazwami - zmienia się tylko przestrzeń nazw (a dla napisów szerokich dochodzi nagłówek
fmt/xchar.h):
Rzeczy, które są tylko w standardzie:
Poza tym specyfikacja formatu jest w obu bibliotekach zgodna (wyrównanie i wypełnienie, znak,
#,
0, szerokość, precyzja, systemy liczbowe, NaN i nieskończoności).
Porównanie szybkości
Na koniec sprawdźmy, czy nowe sposoby formatowania są szybsze od starych. Prosty test wypisuje milion linii z trzema wartościami (liczba całkowita, liczba zmiennoprzecinkowa z trzema miejscami po przecinku i napis) na wyjście przekierowane do
/dev/null, żeby mierzyć samo formatowanie, a nie terminal:
#include <chrono>
#include <cstdio>
#include <format>
#include <iomanip>
#include <iostream>
#include <print>
#include <string>
#include <fmt/core.h>
#include <fmt/printf.h>
int main( int argc, char * * argv )
{
constexpr int N = 1'000'000;
const std::string mode = argc > 1 ? argv[ 1 ]: "";
const std::string text = "abcdef";
auto start = std::chrono::steady_clock::now();
if( mode == "printf" )
for( int i = 0; i < N; ++i )
std::printf( "%d %.3f %s\n", i, i * 0.5, text.c_str() );
else if( mode == "cout" )
for( int i = 0; i < N; ++i )
std::cout << i << ' ' << std::fixed << std::setprecision( 3 ) << i * 0.5 << ' ' << text << '\n';
else if( mode == "format" )
for( int i = 0; i < N; ++i )
std::fputs( std::format( "{} {:.3f} {}\n", i, i * 0.5, text ).c_str(), stdout );
else if( mode == "print" )
for( int i = 0; i < N; ++i )
std::print( "{} {:.3f} {}\n", i, i * 0.5, text );
else if( mode == "fmt" )
for( int i = 0; i < N; ++i )
fmt::print( "{} {:.3f} {}\n", i, i * 0.5, text );
else if( mode == "fmtprintf" )
for( int i = 0; i < N; ++i )
fmt::printf( "%d %.3f %s\n", i, i * 0.5, text );
std::fflush( stdout );
auto ms = std::chrono::duration < double, std::milli >( std::chrono::steady_clock::now() - start ).count();
std::fprintf( stderr, "%.0f ms\n", ms );
}
Kompilacja i uruchomienie jednego wariantu (czas trafia na
stderr):
g++ -std=c++23 -O2 bench.cpp -lfmt -o bench
./bench print > /dev/null
Wyniki średnie dla kilku uruchomień na GCC 14:
Wartości zależą od kompilatora, biblioteki standardowej i komputera, więc warto patrzeć raczej na proporcje. W tym teście
std::print jest około 25% szybszy od
printf
, a strumienie około 50% wolniejsze. Biblioteka fmt wypada najlepiej:
fmt::print jest ponad dwa razy szybszy od
printf
, a
fmt::printf, czyli zamiennik
printf
ze starą składnią, około dwa razy szybszy. Wyłączenie synchronizacji ze stdio (
std::ios::sync_with_stdio(false)) poprawia wynik strumieni tylko częściowo (około 430 ms).
Czas kompilacji i rozmiar programu
Szybkość działania to nie wszystko - szablonowy
std::format ma swoją cenę w czasie kompilacji i w rozmiarze pliku wykonywalnego. Zmierzyłem to na najprostszych programach, które wypisują jedną linię z trzema wartościami, tymi samymi sposobami co wyżej. Każdy jest w osobnym pliku:
#include <cstdio>
int main( int argc, char * * )
{
std::printf( "%d %.3f %s\n", argc, argc * 0.5, "abcdef" );
}
#include <iomanip>
#include <iostream>
int main( int argc, char * * )
{
std::cout << argc << ' ' << std::fixed << std::setprecision( 3 ) << argc * 0.5 << ' ' << "abcdef" << '\n';
}
#include <cstdio>
#include <format>
int main( int argc, char * * )
{
std::fputs( std::format( "{} {:.3f} {}\n", argc, argc * 0.5, "abcdef" ).c_str(), stdout );
}
#include <print>
int main( int argc, char * * )
{
std::print( "{} {:.3f} {}\n", argc, argc * 0.5, "abcdef" );
}
#include <fmt/core.h>
int main( int argc, char * * )
{
fmt::print( "{} {:.3f} {}\n", argc, argc * 0.5, "abcdef" );
}
Programy kompilujemy z
-O2, a rozmiar mierzymy po usunięciu symboli:
time g++ -std=c++23 -O2 print.cpp -o print
strip print
ls -l print
# fmt jako skompilowana biblioteka oraz w trybie header-only
time g++ -std=c++23 -O2 fmt.cpp -lfmt -o fmt
time g++ -std=c++23 -O2 -DFMT_HEADER_ONLY fmt.cpp -o fmt_header
Pozostałe pliki kompilujemy tak samo, zmieniając nazwy.
Wyniki (mediana z 7 kompilacji, GCC 14, Linux, biblioteka standardowa podłączona dynamicznie):
Wnioski: nagłówki
<format> i
<print> wydłużają kompilację zwykłego pliku do około 3 sekund (a
<iostream> do około jednej), a program rośnie o około 85 kB, bo kod formatujący jest wstawiany do programu. Warto więc dołączać je tylko w tych plikach, w których faktycznie coś formatujemy. Biblioteka fmt używana jako skompilowana biblioteka kompiluje się w programie bardzo szybko (0,15 s), ale samą bibliotekę trzeba skompilować raz (u mnie około 4 sekund) i dokłada ona około 125 kB. Tryb header-only kosztuje czas w każdym pliku, który dołącza nagłówki fmt.
Podsumowanie
Do formatowania tekstu w
C++ mamy dziś trzy główne drogi (plus starsze
printf
), a wybór zależy od kompilatora, standardu i potrzeb projektu:
Którego rozwiązania użyć?
Na koniec trzy rzeczy, o których łatwo zapomnieć: napis formatujący musi być znany w czasie kompilacji (inaczej potrzebny jest
vformat albo
dynamic_format), napisy są domyślnie dosuwane do lewej (w
printf
do prawej), a liczby ujemne w
{:x} są wypisywane ze znakiem minus, a nie jako zapis bitowy.
Materiały od autora biblioteki
Autor fmt, Victor Zverovich, prowadzi bloga
vitaut.net, na którym opisuje wiele zagadnień z pogranicza formatowania i szybkiej konwersji liczb na tekst. Wpisy związane z tematem tego artykułu:
W zakładce
Talks są wystąpienia konferencyjne, z których najważniejsze to
Bibliografia
Autor artykułu
Artykuł powstał w X 2026.