Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Inne artykuły

C++ formatowanie wyjścia: strumienie, printf, std::format, std::print i fmt::format

[artykuł] Przegląd formatowania tekstu w C++: od strumieni i printf, przez std::format i std::print (C++20/23), po bibliotekę fmt, na której wzorowano standard. Tabele porównawcze, własne formattery, zakresy, daty, ustawienia lokalne oraz pomiary szybkości i czasu kompilacji. Dla programistów znających strumienie i printf.

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:
  • wprowadzenie w std::format - od zera
  • możliwości formatowania w standardzie: budowa specyfikacji, wyrównanie, liczby (precyzja, systemy liczbowe, flagi, NaN), napisy i znaki, czas i daty, kontenery, ustawienia lokalne, argumenty pozycyjne
  • gdzie trafia wynik (format_to, print, println) oraz sprawdzanie napisu formatującego w czasie kompilacji
  • własne formattery i typy, których nie da się sformatować domyślnie
  • tabele porównawcze: strumienie i
    printf
     kontra std::format
  • biblioteka fmt: skąd ją wziąć, jak zbudować i podłączyć do projektu
  • funkcje biblioteki fmt, których nie ma w standardzie (jeszcze)
  • to co ma standard, ale nie ma w bibliotece fmt
  • porównanie szybkości, czasu kompilacji i rozmiaru programów, podsumowanie oraz materiały od autora biblioteki fmt

Standardowe formatowanie

Funkcje formatowania sprowadzają się do kilku szablonów funkcji. Jednym z nich jest:
C/C++
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:
C/C++
#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:
C/C++
#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>:
C/C++
#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:
Element Znaczenie
numer argumentu (przed dwukropkiem) wybiera argument według numeru zamiast kolejnego w kolejności (rozdział "Argumenty pozycyjne")
wypełnienie i wyrównanie opcjonalny znak wypełnienia i jeden ze znaków <, >, ^ (rozdział "Wyrównanie tekstu")
znak +, - albo spacja: czy pokazywać znak liczby (rozdział "Pozostałe flagi")
# forma alternatywna: prefiks systemu liczbowego lub kropka dziesiętna
0 dopełnianie zerami zamiast spacjami
szerokość minimalna liczba znaków: liczba albo {}, gdy ma pochodzić z argumentu
. i precyzja dla liczb zmiennoprzecinkowych liczba cyfr, a dla napisów maksymalna długość
L uwzględnienie ustawień lokalnych (rozdział "Ustawienia lokalne")
typ literka określająca sposób wypisania, np. d, x, b, f, e, g, s, c, p, ?
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:
C/C++
#include <print>

int main()
{
   
std::println( "{:*>+12.3f}", 3.14159 ); // ******+3.142
}
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:
  • :N - zarezerwuj co najmniej N znaków (bo zmienna może wymagać więcej)
  • < - wyrównanie do lewej (domyślne dla napisów), umieszczane za : (dwukropkiem) przed liczbą znaków
  • > - wyrównanie do prawej (domyślne dla liczb), umieszczane za : (dwukropkiem) przed liczbą znaków
  • ^ - centrowanie (na ile możliwe), umieszczane za : (dwukropkiem) przed liczbą znaków
  • Możliwe jest też ustawienie wypełnienia dowolnym znakiem, wstawiamy go wtedy za : (dwukropek) i przed ewentualnym wyrównaniem
Dla osób znających strumienie poniższa tabela pokazuje, jak uzyskać to samo w obu światach (dla pola o szerokości 8):
strumienie std::format
std::setw(8) {:8} (liczby do prawej, jak w strumieniu, ale napisy do lewej - strumień dosuwa do prawej wszystko)
std::left << std::setw(8) {:<8}
std::right << std::setw(8) {:>8}
brak odpowiednika {:^8} (centrowanie)
std::setfill('*') << std::setw(8) {:*>8} (znak wypełnienia podajemy przed znakiem wyrównania)
std::internal << std::setfill('0') << std::setw(8) {:08} (zera po znaku liczby)
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:
C/C++
#include <format>
#include <iostream>

int main()
{
   
constexpr int date = 1683; // Jan III Sobieski pod Wiedniem
   
std::cout << std::format( "|{:6}|\n", date ); //   wydruk "|  1683|"
   
std::cout << std::format( "|{:2}|\n", date ); //   wydruk "|1683|"
   
std::cout << std::format( "|{:<7}|\n", date ); //  wydruk "|1683   |"
   
std::cout << std::format( "|{:>7}|\n", date ); //  wydruk "|   1683|"
   
std::cout << std::format( "|{:^7}|\n", date ); //  wydruk "| 1683  |"
   
std::cout << std::format( "|{:*^6}|\n", date ); // wydruk "|*1683*|"
   
std::cout << std::format( "|{:.^8}|\n", date ); // wydruk "|..1683..|"
}

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):
C/C++
#include <iostream>
#include <iomanip>  
// std::setw
using namespace std;

int main()
{
   
cout << '|' << setw( 10 ) << right << 1 << "|\n"; //      |         1|
   
cout << '|' << setw( 10 ) << internal << 1 << "|\n"; //   |         1|
   
cout << '|' << setw( 10 ) << left << 1 << "|\n"; //       |1         |
   
   
cout << '|' << setw( 10 ) << right << - 2 << "|\n"; //    |        -2|
   
cout << '|' << setw( 10 ) << internal << - 2 << "|\n"; // |-        2|
   
cout << '|' << setw( 10 ) << left << - 2 << "|\n"; //     |-2        |
   
   
cout << showpos;
   
   
cout << '|' << setw( 10 ) << right << 3 << "|\n"; //      |        +3|
   
cout << '|' << setw( 10 ) << internal << 3 << "|\n"; //   |+        3|
   
cout << '|' << setw( 10 ) << left << 3 << "|\n"; //       |+3        |
}
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):
C/C++
#include <print>

int main()
{
   
std::println( "|{:10}|", - 2 ); //   |        -2|
   
std::println( "|{:<10}|", - 2 ); //  |-2        |
   
std::println( "|{:010}|", - 2 ); //  |-000000002|
   
std::println( "|{:+010}|", 3 ); //   |+000000003|
   
std::println( "|{:*>10}|", - 2 ); // |********-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):
C/C++
#include <iostream>
#include <iomanip>
#include <numbers>
using namespace std;

int main()
{
   
cout << numbers::pi_v < double > << '\n'; //               3.14159
   
cout << fixed << numbers::pi_v < double > << '\n'; //      3.141593
   
cout << scientific << numbers::pi_v < double > << '\n'; // 3.141593e+00
   
cout << hexfloat << numbers::pi_v < double > << '\n'; //   0x1.921fb54442d18p+1
   
   
cout << "Domyślna precyzja: " << cout.precision() << endl; // Domyślna precyzja: 6
   
cout << setprecision( 3 );
   
   
cout << defaultfloat << numbers::pi_v < double > << '\n'; // 3.14
   
cout << fixed << numbers::pi_v < double > << '\n'; //        3.142
   
cout << scientific << numbers::pi_v < double > << '\n'; //   3.142e+00
   
cout << hexfloat << numbers::pi_v < double > << '\n' << endl; // 0x1.921fb54442d18p+1
   
   
cout << setprecision( 2 );
   
   
cout << defaultfloat << - numbers::pi_v < double > << '\n'; // -3.1
   
cout << fixed << - numbers::pi_v < double > << '\n'; //        -3.14
   
cout << scientific << - numbers::pi_v < double > << '\n'; //   -3.14e+00
   
cout << hexfloat << - numbers::pi_v < double > << '\n'; // -0x1.921fb54442d18p+1
   
   
float f = numbers::pi_v < float > * 10e6f;
   
   
cout << defaultfloat << f << '\n'; // 3.1e+07
   
cout << fixed << f << '\n'; //        31415928.00
   
cout << scientific << f << '\n'; //   3.14e+07
   
cout << hexfloat << f << '\n' << endl; // 0x1.df5e78p+24
   
   
cout << defaultfloat << - f << '\n'; // -3.1e+07
   
cout << fixed << - f << '\n'; //        -31415928.00
   
cout << scientific << - f << '\n'; //   -3.14e+07
   
cout << hexfloat << - f << '\n' << endl; // -0x1.df5e78p+24
   
   
f /= 10e8;
   
   
cout << defaultfloat << f << '\n'; // 0.031
   
cout << fixed << f << '\n'; //        0.03
   
cout << scientific << f << '\n'; //   3.14e-02
   
cout << hexfloat << f << '\n' << endl; // 0x1.015bfap-5
   
   
cout << defaultfloat << - f << '\n'; // -0.031
   
cout << fixed << - f << '\n'; //        -0.03
   
cout << scientific << - f << '\n'; //   -3.14e-02
   
cout << hexfloat << - f << '\n' << endl; // -0x1.015bfap-5
}
Jak widać:
  • std::hexfloat jest niewrażliwy na precyzję - zawsze wypisuje tyle cyfr, ile potrzeba do wyświetlenia liczby,
  • std::defaultfloat - precyzja oznacza liczbę cyfr znaczących. Sam wybiera pomiędzy zapisem stałym a naukowym (tak jak %g z języka C): naukowy, gdy wykładnik jest mniejszy niż -4 albo nie mniejszy niż precyzja (stąd 3.1e+07), i nie dopisuje zbędnych zer na końcu,
  • std::fixed - precyzja oznacza liczbę cyfr po przecinku,
  • std::scientific - precyzja oznacza liczbę cyfr po przecinku w mantysie, czyli przed e.
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"):
strumienie std::format
defaultfloat g (albo brak literki, ale z podaną precyzją)
fixed f
scientific e
hexfloat a
setprecision(3) .3, np. {:.3f}
setw(10) 10, np. {:10.3f}
uppercase wielka litera: G, E, A (zmienia e na E)
Ten sam przykład, tym razem z std::format (przez std::println):
C/C++
#include <print>
#include <numbers>

int main()
{
   
constexpr double pi = std::numbers::pi_v < double >;
   
   
std::println( "{}", pi ); //   3.141592653589793
   
std::println( "{:g}", pi ); // 3.14159
   
std::println( "{:f}", pi ); // 3.141593
   
std::println( "{:e}", pi ); // 3.141593e+00
   
std::println( "{:a}", pi ); // 1.921fb54442d18p+1
   
   
std::println( "{:.3g}", pi ); // 3.14
   
std::println( "{:.3f}", pi ); // 3.142
   
std::println( "{:.3e}", pi ); // 3.142e+00
   
std::println( "{:.3a}", pi ); // 1.922p+1
   
   
std::println( "{:.2g}", - pi ); // -3.1
   
std::println( "{:.2f}", - pi ); // -3.14
   
std::println( "{:.2e}", - pi ); // -3.14e+00
   
std::println( "{:.2a}", - pi ); // -1.92p+1
   
   
float f = std::numbers::pi_v < float > * 10e6f;
   
   
std::println( "{:.2g}", f ); // 3.1e+07
   
std::println( "{:.2f}", f ); // 31415928.00
   
std::println( "{:.2e}", f ); // 3.14e+07
   
std::println( "{:.2a}", f ); // 1.dfp+24
   
   
f /= 10e8f;
   
   
std::println( "{:.2g}", f ); // 0.031
   
std::println( "{:.2f}", f ); // 0.03
   
std::println( "{:.2e}", f ); // 3.14e-02
   
std::println( "{:.2a}", f ); // 1.01p-5
}
Kilka różnic względem strumieni, o których warto pamiętać:
  • {} bez precyzji i bez literki wypisuje najkrótszy zapis, który po wczytaniu da dokładnie tę samą liczbę (dla pi: 3.141592653589793). Strumień ma domyślną precyzję 6, dlatego pisze 3.14159. Odpowiednikiem strumieniowego zachowania jest {:g} (skąd tyle cyfr - wyjaśniamy niżej),
  • {:.3} (z precyzją, ale bez literki) zachowuje się jak {:.3g},
  • precyzja obowiązuje tylko dla tego jednego argumentu - nic się nie "lepi" do kolejnych wypisań,
  • {:a} uwzględnia precyzję i zaokrągla (1.922p+1), a hexfloat ją ignoruje,
  • {:a} nie dopisuje prefiksu 0x (bo według specyfikacji działa jak std::to_chars w trybie szesnastkowym), a hexfloat go dopisuje. Jeśli chcemy prefiks, dopisujemy go sami: "0x{:a}",
  • szerokość i precyzję możemy podać jako kolejne argumenty w zagnieżdżonych klamrach: std::println("|{:{}.{}f}|", pi, 12, 4); wypisze liczbę 3.1416 dosuniętą do prawej w polu o szerokości 12 znaków.
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}.
C/C++
#include <print>
#include <numbers>

int main()
{
   
std::println( "{}", std::numbers::pi_v < double > ); // 3.141592653589793
   
std::println( "{}", std::numbers::pi_v < float > ); //  3.1415927
   
std::println( "{}", 0.1 ); // 0.1
   
std::println( "{}", 0.1 + 0.2 ); // 0.30000000000000004
   
std::println( "{}", 1.0 / 3.0 ); // 0.3333333333333333
   
std::println( "{}", 100.0 ); // 100
   
std::println( "{:g}", 0.1 + 0.2 ); // 0.3
   
std::println( "{:.17}", 0.1 ); // 0.10000000000000001
}

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:
C/C++
#include <iostream>

int main()
{
   
std::cout << 64 << '\n'; //                  64
   
std::cout << std::dec << 64 << '\n'; //      64
   
std::cout << std::oct << 64 << '\n'; //      100
   
std::cout << std::hex << 64 << std::endl; // 40
   
   
std::cout << std::showbase;
   
   
std::cout << std::dec << 64 << '\n'; //         64
   
std::cout << std::oct << 64 << '\n'; //       0100
   
std::cout << std::hex << 64 << std::endl; // 0x40
}
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:
C/C++
#include <print>

int main()
{
   
std::println( "{} {:d} {:o} {:x}", 64, 64, 64, 64 ); // 64 64 100 40
   
std::println( "{:#d} {:#o} {:#x} {:#b}", 64, 64, 64, 64 ); // 64 0100 0x40 0b1000000
   
std::println( "{:X} {:#X} {:B} {:#B}", 255, 255, 5, 5 ); // FF 0XFF 101 0B101
   
std::println( "|{:#010x}|", 255 ); // |0x000000ff|
   
std::println( "|{:08b}|", 22 ); // |00010110|
   
std::println( "|{:>#10x}|", 255 ); // |      0xff|
}
Odpowiedniki zebrane w tabeli:
strumienie std::format
std::dec {:d} (albo samo {})
std::oct {:o}
std::hex {:x}
std::hex << std::uppercase {:X}
std::showbase << std::hex {:#x}
std::showbase << std::oct {:#o}
std::hex << std::setfill('0') << std::setw(8) {:08x}
brak (trzeba użyć std::bitset) {:b}, {:#b}, {:08b}
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:
C/C++
#include <iostream>
#include <format>

int main()
{
   
std::cout << std::hex << - 1 << '\n'; // ffffffff
   
std::cout << std::format( "{:x}", - 1 ) << '\n'; // -1
   
std::cout << std::format( "{:x}", static_cast < unsigned >( - 1 ) ) << '\n'; // ffffffff
}

Pozostałe flagi

Z czasów przed std::format mamy kilka innych możliwości formatowania:
  • std::boolalpha (wypisuje wartości bool jako true i false zamiast 1 i 0),
  • std::showpoint (zawsze pokazuje kropkę dziesiętną i końcowe zera, nawet gdy liczba jest całkowita, np. 1.00000 zamiast 1),
  • std::uppercase (używa wielkich liter w liczbach szesnastkowych, zapisie naukowym i prefiksach, np. FF, 1E+10, 0XFF)
  • std::showpos - pokazuje znak + również dla liczb dodatnich,
  • std::showbase - pokazuje prefiks systemu liczbowego: 0 dla ósemkowego i 0x dla szesnastkowego
Dla strumieni wyglądają tak:
C/C++
#include <iostream>
#include <iomanip>

int main()
{
   
std::cout << true << '\n'; // 1
   
std::cout << std::boolalpha << true << '\n'; // true
   
std::cout << std::noboolalpha;
   
   
std::cout << 1.0 << '\n'; // 1
   
std::cout << std::showpoint << 1.0 << '\n'; // 1.00000
   
std::cout << std::noshowpoint;
   
   
std::cout << std::hex << 255 << '\n'; // ff
   
std::cout << std::uppercase << 255 << '\n'; // FF
   
std::cout << std::showbase << 255 << '\n'; // 0XFF
   
std::cout << std::dec << std::nouppercase << std::noshowbase;
   
   
std::cout << 3 << '\n'; // 3
   
std::cout << std::showpos << 3 << '\n'; // +3
}
A w std::format:
C/C++
#include <print>

int main()
{
   
std::println( "{}", true ); // true
   
std::println( "{:d}", true ); // 1
   
   
std::println( "{}", 1.0 ); // 1
   
std::println( "{:#g}", 1.0 ); // 1.00000
   
std::println( "{:#}", 1.0 ); // 1.
   
   
std::println( "{:x}", 255 ); // ff
   
std::println( "{:X}", 255 ); // FF
   
std::println( "{:#X}", 255 ); // 0XFF
   
   
std::println( "{}", 3 ); // 3
   
std::println( "{:+}", 3 ); // +3
   
std::println( "{: }", 3 ); //  3
}
Zestawienie odpowiedników:
strumienie std::format
std::boolalpha w std::format domyślnie jest odwrotnie: bool wypisuje się jako true/false ({}), a liczbę dostajemy przez {:d}
std::showpoint #: z literką g ({:#g}) zostawia końcowe zera jak strumień, a bez literki ({:#}) dopisuje samą kropkę
std::uppercase wielka litera typu (X, E, G, A, B), więc można to zrobić osobno dla każdej liczby
std::showpos {:+} (albo {: }, gdy zamiast plusa ma być spacja)
std::showbase # z literką systemu liczbowego, np. {:#x}

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}):
C/C++
#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 ); // inf nan -inf
   
std::println( "{:f} {:F}", inf, inf ); // inf INF
   
std::println( "{:g} {:G}", nan, nan ); // nan NAN
   
std::println( "|{:010}|{:010}|", 3.5, inf ); // |00000003.5|       inf|
   
std::println( "|{:<010}|", 3.5 ); // |3.5       |
}
Strumienie wypisują te wartości podobnie, ale wypełnienie zerami zachowuje się inaczej - dopełnia zerami także inf:
C/C++
#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'; // inf nan -inf
   
std::cout << std::uppercase << inf << ' ' << nan << std::nouppercase << '\n'; // INF NAN
   
std::cout << std::showpos << inf << std::noshowpos << '\n'; // +inf
   
std::cout << std::setfill( '0' ) << std::setw( 10 ) << 3.5 << '\n'; // 00000003.5
   
std::cout << std::setfill( '0' ) << std::setw( 10 ) << inf << '\n'; // 0000000inf
}
strumienie std::format
inf, nan (małymi literami) {}, {:f}, {:g} - to samo
std::uppercase - INF, NAN wielka litera typu: {:F}, {:E}, {:G}
std::showpos - +inf {:+} - +inf
std::setfill('0') << std::setw(10) - 0000000inf {:010} - inf poprzedzone spacjami, bez zer

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:
C/C++
#include <iostream>
#include <format>
#include <iomanip>  
// std::setprecision
#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 ); // 1234567
   
std::println( "{:L}", v1 ); // 1234567
   
   
std::cout << std::format( pl, "{:L}", v1 ) << '\n'; // 1 234 567
   
std::cout << std::format( pl, "{:.1Lf}", v2 ) << '\n'; // 1 234 567,5
   
   
std::cout << std::fixed << std::setprecision( 1 );
   
std::cout << v1 << " and " << v2 << '\n'; // 1234567 and 1234567.5
   
std::cout.imbue( pl );
   
std::cout << v1 << " and " << v2 << '\n'; // 1 234 567 and 1 234 567,5
}
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:
C/C++
#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 ); // 1234567
   
std::println( "{}", std::format( pl, "{:L}", 1234567 ) ); // 1 234 567
   
std::println( "{}", std::format( pl, "{:.2Lf}", 1234.5 ) ); // 1 234,50
   
std::println( "{}", std::format( pl, "{}", 1234567 ) ); // 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:
C/C++
#include <print>

int main()
{
   
std::println( "|{:.3}|", "abcdef" ); // |abc|
   
std::println( "|{:8.3}|", "abcdef" ); // |abc     |
   
std::println( "|{:>8.3}|", "abcdef" ); // |     abc|
}

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:
C/C++
#include <print>

int main()
{
   
std::println( "{} {:d} {:x}", 'A', 'A', 'A' ); // A 65 41
   
std::println( "{:c} {:c}", 65, 0x41 ); // A A
   
std::println( "{} {}", static_cast < unsigned char >( 'A' ), static_cast < signed char >( 'A' ) ); // 65 65
}

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ć.
C/C++
#include <print>
#include <string>

int main()
{
   
std::println( "{:?} {:?}", "a\tb\"c", '\n' ); // "a\tb\"c" '\n'
   
std::println( "{:?}", std::string( "zażółć\n" ) ); // "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).
C/C++
#include <print>

int main()
{
   
std::println( "|{:6}|", "abcd" ); // |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.
C/C++
#include <print>

int main()
{
   
int x = 0;
   
int * ptr = & x;
   
const char * text = "tekst";
   
   
std::println( "{}", nullptr ); // 0x0
   
std::println( "{}", static_cast < void * >( nullptr ) ); // 0x0
   
std::println( "{}", static_cast < const void * >( ptr ) ); // adres zmiennej x, np. 0x7ffda5a0d854 (inny przy każdym uruchomieniu)
   
std::println( "{}", text ); // tekst
   
std::println( "{}", static_cast < const void * >( text ) ); // adres napisu, np. 0x5581c0a2b004 (inny przy każdym uruchomieniu)
}
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:
C/C++
#include <iostream>

int main()
{
   
int x = 0;
   
int * ptr = & x;
   
const char * text = "tekst";
   
   
std::cout << ptr << '\n'; // adres zmiennej x, np. 0x7ffd5e0c3b4c
   
std::cout << text << '\n'; // tekst
   
std::cout << static_cast < const void * >( text ) << '\n'; // adres napisu, np. 0x5581c0a2b004
   
std::cout << nullptr << '\n'; // nullptr
   
std::cout << static_cast < void * >( nullptr ) << '\n'; // 0
}

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 %:
Znak Znaczenie
%Y rok, czterocyfrowy (np. 1683)
%m miesiąc jako liczba (01-12)
%d dzień miesiąca (01-31)
%H godzina (00-23)
%M minuta (00-59)
%S sekunda (00-59)
%A, %a nazwa dnia tygodnia: pełna i skrócona (np. Sunday, Sun)
%B, %b nazwa miesiąca: pełna i skrócona (np. September, Sep)
%j dzień roku (001-366)
%F skrót na %Y-%m-%d
%T skrót na %H:%M:%S
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.
C/C++
#include <chrono>
#include <print>

int main()
{
   
using namespace std::chrono;
   
   
sys_days battle = year { 1683 } / September / 12;
   
std::println( "{}", battle ); // 1683-09-12
   
std::println( "{:%d.%m.%Y}", battle ); // 12.09.1683
   
std::println( "{:%A}", battle ); // Sunday
   
   
seconds elapsed { 3725 };
   
std::println( "{}", elapsed ); // 3725s
   
std::println( "{:%H:%M:%S}", elapsed ); // 01:02:05
   
std::println( "{}", 1500ms ); // 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:
C/C++
#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 ) ); // niedziela, 12 września 1683
   
std::println( "{}", std::format( pl, "{:L%a %d %b}", battle ) ); // nie 12 wrz
   
std::println( "{}", std::format( pl, "{:%A}", battle ) ); // Sunday
}

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).
C/C++
#include <map>
#include <print>
#include <string>
#include <tuple>
#include <vector>

int main()
{
   
std::vector < int > v { 1, 2, 3 };
   
std::println( "{}", v ); // [1, 2, 3]
   
std::println( "{:n}", v ); // 1, 2, 3
   
std::println( "{::02}", v ); // [01, 02, 03]
   
std::println( "{::x}", std::vector < int > { 10, 11, 255 } ); // [a, b, ff]
   
std::println( "{}", std::vector < std::string > { "a", "b c" } ); // ["a", "b c"]
   
std::println( "{}", std::map < int, std::string > { { 1, "x" }, { 2, "y" } } ); // {1: "x", 2: "y"}
   
std::println( "{}", std::pair { 1, 'a' } ); // (1, 'a')
   
std::println( "{}", std::tuple { 1, 2.5, "t" } ); // (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:
C/C++
#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"; // [1, 2, 3]
   
   
std::ranges::copy( v, std::ostream_iterator < int >( std::cout, ", " ) ); // 1, 2, 3,
   
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"; // [01, 02, 03]
}

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ć:
Wartość Sposób wypisania
sequence w nawiasach kwadratowych
set w klamrach: {1, 2, 3}
map w klamrach, pary z dwukropkiem: {1: "x", 2: "y"}
string, debug_string zakres znaków jak napis (druga wartość w wersji z cudzysłowami i sekwencjami ucieczki)
disabled zakres w ogóle nie jest formatowany
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:
C/C++
#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 >> ) ] ); // sequence
   
std::println( "{}", names[ static_cast < int >( std::format_kind < std::set < int >> ) ] ); // set
   
std::println( "{}", names[ static_cast < int >( std::format_kind < std::map < int, int >> ) ] ); // map
   
std::println( "{}", names[ static_cast < int >( std::format_kind < NumberList > ) ] ); // sequence
   
   
std::println( "{}", NumberList { { 1, 2, 3 } } ); // [1, 2, 3]
   
std::println( "{}", NumberSet { { 1, 2, 3 } } ); // {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}).
C/C++
#include <print>

int main()
{
   
std::println( "{} {}", "a", "b" ); // a b
   
std::println( "{1} {0}", "a", "b" ); // b a
   
std::println( "{0} {0} {0}", "hej" ); // hej hej hej
   
std::println( "{0} {2}", "a", "b", "c" ); // a c
   
   
std::println( "|{:>{}}|", "x", 5 ); // |    x|
   
std::println( "|{0:>{1}}|", "x", 5 ); // |    x|
   
std::println( "|{0:{1}.{2}f}|", 3.14159, 10, 2 ); // |      3.14|
   
   
std::println( "{0:d} {0:o} {0:b} {0:x}", 42 ); // 42 52 101010 2a
   
   
std::println( "{{}} {{{}}}", 42 ); // {} {42}
}
Zasady:
  • w jednym napisie nie wolno mieszać numerowania automatycznego ({}) z ręcznym ({0}) - to błąd kompilacji, np. std::format("{} {1}", "a", "b"),
  • numer musi wskazywać istniejący argument, więc std::format("{2}", "a", "b") też jest błędem kompilacji (liczymy od 0),
  • nie każdy argument musi zostać użyty,
  • szerokość i precyzję można pobrać z argumentu, wstawiając w miejsce liczby puste klamry albo numer w klamrach: {:>{}} bierze wartość, a potem szerokość, natomiast {0:>{1}} wskazuje je numerami. Zakaz mieszania numerowania dotyczy też tych zagnieżdżonych klamer,
  • klamry jako zwykłe znaki wypisujemy podwajając je: {{ daje znak {, a }} daje znak }. Dlatego ostatnia linia przykładu, "{{}} {{{}}}", jest czytana od lewej tak: {{ i }} to dwa zwykłe znaki, czyli {}; potem jest spacja; w {{{}}} pierwsze {{ to znak {, następne {} to prawdziwe pole na argument (dostaje 42), a ostatnie }} to znak }. Wynik to {} {42}.
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:
  • std::format_to(it, ...) - zapisuje wynik przez iterator wyjściowy (np. std::back_inserter(str) dopisuje na końcu napisu). Nie sprawdza rozmiaru bufora, więc sami musimy zadbać o jego odpowiedni rozmiar,
  • std::format_to_n(it, n, ...) - jak wyżej, ale zapisze najwyżej n znaków (i nie dopisze znaku końca napisu). Zwraca iterator za ostatnim zapisanym znakiem oraz liczbę znaków, które zająłby pełny wynik,
  • std::formatted_size(...) - tylko oblicza, ile znaków zajmie wynik, bez jego zapisywania (można dzięki temu przygotować bufor o dokładnym rozmiarze).
C/C++
#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 ); // 1-2|   3
   
   
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 ); // 3.142 i dluga r (pelny wynik: 20 znakow)
   
   
std::println( "{}", std::formatted_size( "{:>10}", 1 ) ); // 10
}

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).
C/C++
#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(); // pusta linia, to dozwolone od C++26
   
   
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:
C/C++
#include <format>
#include <string>

int main()
{
   
std::string s1 = std::format( "{:d}", "tekst" ); // błąd: tekstu nie da się wypisać jako liczby
   
std::string s2 = std::format( "{} {}", 1 ); // błąd: za mało argumentów
}
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:
  • literka typu musi pasować do typu argumentu: d, x, o, b są dla liczb całkowitych (także bool i char), f, e, g, a dla liczb zmiennoprzecinkowych, s dla napisów i bool, c dla znaków i liczb całkowitych, a p dla wskaźników. Błędem jest więc {:d} dla napisu, {:s} albo {:f} dla int, {:x} lub {:c} dla double i {:p} dla int,
  • znak, # i 0 mają sens tylko dla liczb, więc {:+}, {:#} albo {:010} dla napisu to błąd,
  • precyzja jest dozwolona dla liczb zmiennoprzecinkowych i napisów, ale nie dla liczb całkowitych: {:.2} dla int to błąd,
  • L jest dozwolone tylko dla liczb ({:L} dla napisu to błąd),
  • ? (C++23) jest dozwolone tylko dla napisów i znaków.
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.
C/C++
#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:
  • std::vformat(format_str, std::make_format_args(a, b)) - wersja bez sprawdzania typów: argumenty pakujemy w obiekt, który zapamiętuje tylko referencje do nich (dlatego muszą to być zmienne, a nie tymczasowe wartości). Zwraca std::string. Jest dostępna od C++20, a std::format jest po prostu jej nakładką. Analogiczne wersje mają std::format_to (std::vformat_to) oraz std::print (std::vprint_unicode i std::vprint_nonunicode),
  • std::dynamic_format(format_str) - opakowuje napis tak, że można go podać w miejsce zwykłego napisu formatującego w std::format, std::print, std::println i pozostałych funkcjach. Wywołanie wygląda jak zwykłe, argumenty mogą być tymczasowe, a ich typy są znane kompilatorowi. Nie jest sprawdzany tylko sam napis - błędy zgłaszane są wyjątkiem std::format_error.

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:
C/C++
#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" ) ) ); // 42|x
   
std::println( "{}", std::format( std::dynamic_format( format_str ), 42, "x" ) ); //    42|x
   
std::println( std::dynamic_format( format_str ), 7, "y" ); //      7|y
   
   
log( "{} + {} = {}", 2, 3, 5 ); // [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):
C/C++
#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:
  • parse - dostaje część napisu formatującego po dwukropku (np. >12 z {:>12}), sprawdza ją i zwraca iterator na zamykającą klamrę. Dla napisu znanego w czasie kompilacji wykonuje się to już podczas kompilacji, dlatego metoda jest constexpr,
  • format - dostaje obiekt i kontekst, zapisuje tekst do ctx.out() i zwraca iterator za ostatnim zapisanym znakiem. Metoda musi być const.

Najprostszy przykład nie obsługuje żadnych opcji (po dwukropku nic nie można podać):
C/C++
#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 ); // (1.5, -2)
}
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:
C/C++
#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 ); //    |zielony|
   
std::println( "|{:>12}|", Color::blue ); // |   niebieski|
   
std::println( "|{:.^12}|", Color::red ); // |..czerwony..|
}
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):
C/C++
#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 ); // (1.5, -2)
   
std::println( "{:.2f}", p ); // (1.50, -2.00)
   
std::println( "{:+8.1e}", p ); // (+1.5e+00, -2.0e+00)
}

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:
  • wskaźniki inne niż void*, const void*, nullptr i const char* (ten ostatni jest napisem) - rzutujemy na const void*,
  • enum i enum class - rzutujemy na typ całkowity (std::to_underlying z C++23) albo piszemy własny formatter, jak dla Color wyżej,
  • napisy UTF-8 typu char8_t (u8"...") oraz znaki wchar_t w zwykłym (wąskim) napisie formatującym - szerokie znaki wymagają szerokich wersji funkcji, np. std::wstring i napisu L"...",
  • typy bez formattera, np. std::bitset (pomaga to_string()) i std::error_code (pomaga message()),
  • własne klasy - pomaga własny formatter, opisany wyżej.

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):
printf std::format
%d {} (typ jest rozpoznawany sam, więc %zu, %lld itp. też to samo {})
%5d {:5}
%-5d {:<5}
%05d {:05}
%+d {:+}
%x, %#x, %X, %o {:x}, {:#x}, {:X}, {:o}
%f, %.2f, %8.3f {:f}, {:.2f}, {:8.3f}
%e, %g {:e}, {:g}
%s {} (działa też dla std::string, bez c_str())
%10s {:>10}
%-10s {:10} albo {:<10}
%.3s {:.3}
%c {}
%% % (bez zmian, specjalne są tylko klamry)
%*d {:{}} (uwaga: najpierw wartość, potem szerokość)
%.*f {:.{}f} (najpierw wartość, potem precyzja)
Najważniejsza pułapka dotyczy wyrównania napisów:
  • w printf napis jest domyślnie dosuwany do prawej (%10s),
  • w std::format napis jest domyślnie dosuwany do lewej ({:10}), a dosunięcie do prawej trzeba wprost zażądać znakiem >: {:>10},
  • liczby są dosuwane do prawej w obu przypadkach.
C/C++
#include <cstdio>
#include <print>

int main()
{
   
std::printf( "|%10s|\n", "tekst" ); // |     tekst|
   
std::println( "|{:10}|", "tekst" ); // |tekst     |
   
std::println( "|{:>10}|", "tekst" ); // |     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:
C/C++
#include <format>
#include <print>
#include <string>

int main()
{
   
std::println( "{}", std::to_string( 3.14 ) ); // 3.140000
   
std::println( "{}", std::to_string( 1e-7 ) ); // 0.000000
   
std::println( "{}", std::to_string( 1.0 / 3.0 ) ); // 0.333333
   
   
std::println( "{}", std::format( "{}", 3.14 ) ); // 3.14
   
std::println( "{}", std::format( "{}", 1e-7 ) ); // 1e-07
   
std::println( "{}", std::format( "{:.2f}", 3.14 ) ); // 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:
Menedżer pakietów Polecenie
vcpkg vcpkg install fmt
apt (Debian, Ubuntu) sudo apt install libfmt-dev
Homebrew (macOS) brew install fmt
Conda conda install -c conda-forge fmt
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:
C/C++
#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:
C/C++
#include <fmt/format.h>

int main()
{
   
fmt::print( "Zażółć gęślą jaźń!\n" ); // Zażółć gęślą jaźń!
   
fmt::print( "|{:>16}|\n", "Zażółć gęślą" ); // |    Zażółć gęślą|
   
fmt::print( "|{:^12}|\n", "żółć" ); // |    żółć    |
   
fmt::print( "|{:.4}|\n", "Zażółć gęślą jaźń" ); // |Zażó|
   
fmt::print( "{:?}\n", "zażółć\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:
C/C++
#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 ) ); // Ala ma 20 lat
   
fmt::print( "{x}+{x}\n", "x"_a = 7 ); // 7+7
   
   
std::vector < int > v { 1, 2, 3 };
   
fmt::print( "{}\n", v ); // [1, 2, 3]
   
fmt::print( "{}\n", std::map < std::string, int > { { "a", 1 }, { "b", 2 } } ); // {"a": 1, "b": 2}
   
fmt::print( "{}\n", std::tuple < int, char, std::string > { 1, 'a', "x" } ); // (1, 'a', "x")
   
fmt::print( "{}\n", fmt::join( v, "; " ) ); // 1; 2; 3
   
fmt::print( "{::02}\n", v ); // [01, 02, 03]
   
   
fmt::print( "{}\n", fmt::group_digits( 1234567 ) ); // 1,234,567
}
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:
C/C++
#include <fmt/std.h>
#include <filesystem>
#include <optional>
#include <variant>

int main()
{
   
fmt::print( "{}\n", std::optional < int >( 3 ) ); // optional(3)
   
fmt::print( "{}\n", std::variant < int, double >( 2.5 ) ); // variant(2.5)
   
fmt::print( "{}\n", std::filesystem::path( "/tmp/a.txt" ) ); // /tmp/a.txt (w std dopiero od C++26)
}
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:
C/C++
#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:
C/C++
#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() ) ); // 42
   
   
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 ) ); // 1-2|   3
   
fmt::print( "{} {}\n", buf.size(), buf.capacity() ); // 8 500
   
   
fmt::print( "{}\n", fmt::format_int( 1234567 ).c_str() ); // 1234567
}

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:
C/C++
#include <fmt/chrono.h>
#include <chrono>
#include <ctime>

int main()
{
   
using namespace std::literals::chrono_literals;
   
   
fmt::print( "{} {}\n", 42s, 100ms ); // 42s 100ms
   
fmt::print( "{:%H:%M:%S}\n", 3h + 15min + 30s ); // 03:15:30
   
   
std::tm tm = fmt::gmtime( std::time_t { 0 } );
   
fmt::print( "{:%Y-%m-%d %H:%M:%S}\n", tm ); // 1970-01-01 00:00:00
   
fmt::print( "{:%d.%m.%Y}\n", tm ); // 01.01.1970
   
   
auto now = std::chrono::system_clock::now();
   
fmt::print( "Dzisiaj jest {:%d.%m.%Y}, godzina {:%H:%M}\n", now, now ); // np. Dzisiaj jest 04.10.2026, godzina 21:14
}
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:
C/C++
#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 ) ); // 1 dwa
   
   
std::string s = fmt::format( FMT_COMPILE( "{:>6}|" ), 42 );
   
fmt::print( "{}\n", s ); //     42|
}

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:
C/C++
#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 } ) ); // Stary#7
   
fmt::print( "|{:>12}|\n", fmt::streamed( OldType { 7 } ) ); // |     Stary#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.
C/C++
#include <fmt/printf.h>
#include <cstdio>
#include <string>

int main()
{
   
fmt::printf( "%d + %d = %d\n", 2, 3, 5 ); // 2 + 3 = 5
   
fmt::fprintf( stderr, "Blad: %s\n", "brak pliku" ); // Blad: brak pliku (na stderr)
   
   
std::string s = fmt::sprintf( "%5.2f|%-4d|%s", 3.14159, 42, "x" );
   
fmt::printf( "%s\n", s ); //  3.14|42  |x
   
fmt::printf( "%s|\n", std::string( "bez c_str()" ) ); // bez c_str()|
   
fmt::printf( "%2$s %1$s\n", "a", "b" ); // b a
   
   
try {
       
fmt::printf( "%d\n", "tekst" );
   
} catch( const fmt::format_error & e ) {
       
fmt::printf( "wyjatek: %s\n", e.what() ); // np. wyjatek: invalid format specifier
   
}
}

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):
std fmt
std::format, std::format_to, std::format_to_n, std::formatted_size fmt::format, fmt::format_to, fmt::format_to_n, fmt::formatted_size
std::print, std::println fmt::print, fmt::println
std::vformat, std::make_format_args fmt::vformat, fmt::make_format_args
std::format_string fmt::format_string
std::dynamic_format (C++26) fmt::runtime
std::formatter, std::formattable fmt::formatter, fmt::formattable
std::range_formatter fmt::range_formatter (nagłówek fmt/ranges.h)
std::format_error fmt::format_error
Rzeczy, które są tylko w standardzie:
  • brak dodatkowej zależności - std::format jest częścią biblioteki standardowej,
  • formattery dla std::stacktrace i std::stacktrace_entry (C++23) - w fmt nie ma odpowiednika,
  • inny wynik typu a dla liczb zmiennoprzecinkowych: std::format("{:a}", 1.5) wypisuje 1.8p+0, a fmt::format("{:a}", 1.5) dopisuje prefiks: 0x1.8p+0.
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:
C/C++
#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:
Sposób Czas dla miliona linii
printf 310 ms
std::cout (z std::fixed i std::setprecision) 470 ms
std::format + fputs 290 ms
std::print 230 ms
fmt::printf 160 ms
fmt::print 125 ms
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:
C/C++
// printf.cpp
#include <cstdio>

int main( int argc, char * * )
{
   
std::printf( "%d %.3f %s\n", argc, argc * 0.5, "abcdef" );
}
C/C++
// cout.cpp
#include <iomanip>
#include <iostream>

int main( int argc, char * * )
{
   
std::cout << argc << ' ' << std::fixed << std::setprecision( 3 ) << argc * 0.5 << ' ' << "abcdef" << '\n';
}
C/C++
// format.cpp
#include <cstdio>
#include <format>

int main( int argc, char * * )
{
   
std::fputs( std::format( "{} {:.3f} {}\n", argc, argc * 0.5, "abcdef" ).c_str(), stdout );
}
C/C++
// print.cpp
#include <print>

int main( int argc, char * * )
{
   
std::print( "{} {:.3f} {}\n", argc, argc * 0.5, "abcdef" );
}
C/C++
// fmt.cpp
#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):
Sposób Czas kompilacji / rozmiar programu
printf 60 ms / 14 kB
std::cout 0,9 s / 14 kB
std::format + fputs 3,1 s / 98 kB
std::print 3,1 s / 98 kB
fmt::print (skompilowana biblioteka) 0,15 s / 138 kB
fmt::print (header-only) 4,4 s / 130 kB
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:
  • strumienie (std::cout i manipulatory) - działają wszędzie i są dobrze znane, ale ich ustawienia są "lepkie", składnia jest rozwlekła, a w teście szybkości okazały się najwolniejsze,
  • printf
    - zwięzły i szybki, ale niebezpieczny: niezgodność typów to zachowanie niezdefiniowane, a własnych typów nie da się wypisać,
  • std::format i std::print (C++20 i C++23) - napis formatujący jest sprawdzany w czasie kompilacji, składnia jest jedna dla liczb, napisów, dat i zakresów, a własne typy obsługujemy przez std::formatter. W teście std::print był szybszy od
    printf
     i strumieni, kosztem dłuższej kompilacji i większego pliku wykonywalnego,
  • biblioteka fmt - to, na czym wzorowano standard, z dodatkami, których w nim nie ma (argumenty nazwane, kolory, std::tm, fmt::printf). Działa od C++11 i w teście była najszybsza.

Którego rozwiązania użyć?

  • jeśli kompilator ma nagłówek <print> (C++23), używamy std::print, std::println i std::format zamiast std::cout i
    printf
    ,
  • jeśli jest tylko <format> (C++20), łączymy std::format z std::cout albo fputs,
  • jeśli projekt utknął na starszym standardzie albo potrzebujemy możliwości spoza standardu (kolory, argumenty nazwane, zapis do pliku, szybkość), sięgamy po bibliotekę fmt,
  • w istniejącym kodzie z
    printf
     najmniej pracy wymaga fmt::printf, bo zachowuje dotychczasowe napisy formatujące.
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.
Autor Zakres zmian Afiliacja
Bazior Grzegorz Utworzenie artykułu Pracownik AGH w Krakowie