A charconv, format és C++23 print alfejezet
rögzítette a std::format szerződését és a
std::print megjelenését. Itt a gyakorlati viselkedést
vesszük végig: milyen célra ír a print, hogyan viselkedik
terminálon és átirányított kimeneten, hogyan formázunk tárolókat és
tuple-öket, és milyen csapdát jelent a printf-fel való
keverés. A pufferelés szabályait a stdio, FILE és
pufferelés és az iostream, stream state és
pufferelés alfejezetekből ismertnek vesszük; itt csak a
print szempontjából alkalmazzuk őket.
Fontos keret: az alapprofil -std=c++23, de a
<print> és a range-formázás elérhetősége a standard
library kiadásától függ. A könyv nem állítja, hogy bármely konkrét
GCC/Clang/libstdc++/libc++ verzió támogatja ezeket; minden képességhez
feature-test makrót és compile probe-ot adunk.
Definíció (ISO C++23, <print>). A
std::print és std::println függvénysablonok a
formázott szöveget egy std::FILE* streame írják. A releváns
túlterhelések:
template<class... Args>
void print(std::FILE* stream, std::format_string<Args...> fmt, Args&&... args);
template<class... Args>
void print(std::format_string<Args...> fmt, Args&&... args);
template<class... Args>
void println(std::FILE* stream, std::format_string<Args...> fmt, Args&&... args);
template<class... Args>
void println(std::format_string<Args...> fmt, Args&&... args);Feltételek. (1) fmt-nek konstans
kifejezésnek kell lennie; ha nem az, a program ill-formed, diagnosztika
kötelező. (2) A fmt érvényességét Args...-ra a
fordító ellenőrzi. (3) A stream nélküli túlterhelés hatása
print(stdout, fmt, ...); a println ugyanez
plusz egy '\n' karakter. (4) A
print(stream, ...) hatása a szabvány szerint
vprint_unicode(stream, fmt.get(), std::make_format_args(args...))
— ezt a konkrét megfogalmazást a saját szabványpéldányodon ellenőrizd,
mert a vprint_unicode és vprint_nonunicode
közti választás a megvalósítás és a stream jellege szerint történik.
Nincs ostream túlterhelés. A C++23
print nem ír std::ostream-re. Ha streamre kell
írni, a helyes minta:
std::ostringstream os;
os << std::format("vector: {}\n", std::vector{1, 2, 3});Ez a format + operator<< kombináció:
a formázás std::string-be megy, a kiírás a stream
bufferébe. A print közvetlen FILE*-ra íráskor
elkerüli ezt a köztes allokációt.
Feature-test és compile probe. A
__cpp_lib_print és a __cpp_lib_format_ranges
értékét a <version> fejlécből kell kiolvasni; a
konkrét revízió nem tekinthető igazoltnak.
// probe_print.cpp
#include <version>
#ifndef __cpp_lib_print
#error "std::print nem elerheto"
#elif __cpp_lib_print < 202207L
#error "std::print revizioja regi"
#endif
#ifndef __cpp_lib_format_ranges
#error "range formatting (P2286) nem elerheto"
#endif
#include <print>
int main() { std::println("probe: {}", 42); }g++ -std=c++23 -Wall -Wextra -o probe_print probe_print.cpp
# régebbi driveren: -std=c++2bHa a probe nem fordul, fallback: std::format +
std::fwrite, illetve printf. A fallback
viselkedése azonos, csak a kényelem és a fordítási idejű ellenőrzés vész
el.
A std::print mögötti két alacsony szintű függvény:
std::vprint_unicode(FILE*, std::string_view, std::format_args)
— ha a stream olyan terminálra mutat, amely támogatja a Unicode-ot, és a
natív Unicode API elérhető, a formázott szöveget a natív API-n keresztül
írja ki; egyébként std::fwrite-tal.std::vprint_nonunicode(FILE*, std::string_view, std::format_args)
— mindig std::fwrite-tal ír.Következmény. A formázott string kódolása az
úgynevezett ordinary literal encoding (a forrás- és
végrehajtási karakterkészlet viszonyából), nem a terminál kódolása. A
terminál kódolását az operációs rendszer és a locale állítja be; a
print nem végez transzkódolást. Ha a lokál és a program
kódolása eltér, a megjelenés hibás lesz, a program viszont nem jelez
hibát.
Hibakezelés. A fwrite útvonal hibáját a
stream hibaállapota jelzi (std::ferror), míg a natív
Unicode API hibáját a szabvány std::system_error
kivételként írja le. Ezt a megfogalmazást a saját szabványpéldányodon
ellenőrizd, mert a két útvonal hibajelzése eltér.
A P2286 („Formatting Ranges”) a std::formatter-t
kiterjeszti range-ekre, pair-re és tuple-re. A formátum-specifikáció
? (debug) opciót is elfogadnak a string- és
char-formatterek, valamint a range-formatterek. A range-formázás
alapértelmezésben szögletes zárójelbe tesz, ", "
elválasztóval, és a string elemeket idézőjelbe teszi; a
? a nem nyomtatható karakterek escape-elését kapcsolja
be.
// demo_print.cpp
#include <print>
#include <vector>
#include <map>
#include <tuple>
#include <string>
#include <utility>
int main() {
std::vector<int> v{1, 2, 3};
std::println("vector: {}", v); // [1, 2, 3]
std::vector<std::string> names{"alice", "bob"};
std::println("names: {}", names); // ["alice", "bob"]
auto t = std::make_tuple(1, 2.5, std::string{"x"});
std::println("tuple: {}", t); // (1, 2.5, x)
std::map<std::string, int> m{{"a", 1}, {"b", 2}};
std::println("map: {}", m); // {a: 1, b: 2}
std::vector<std::vector<int>> nested{{1}, {2, 3}};
std::println("nested: {}", nested); // [[1], [2, 3]]
}g++ -std=c++23 -O2 -Wall -Wextra -o demo_print demo_print.cppA megjegyzésben szereplő kimenet illusztráció, nem mért vagy tesztelt eredmény; a saját toolchainen kell ellenőrizni.
Debug-formátum ?. Stringre közvetlenül:
std::format("{}", "hi") → hi,
std::format("{:?}", "hi") → "hi". Range-en
belül a ? az elemekre öröklődik. A ? a skalár
formatterekre (pl. int) jelenleg leggyakrabban hatástalan,
de ezt nem általánosítjuk: a formatter szerződése típusonként dönt.
Fordítási idejű ellenőrzés. A
format_string<Args...> konstans kiértékelése miatt a
hibás specifikáció és a nem formázható argumentumtípus fordításidejű
hiba. Futásidejű formátumsztring C++23-ban csak
std::vformat + std::make_format_args útvonalon
lehetséges, ellenőrzés nélkül. A std::runtime_format
(P2918) C++26, itt nem használjuk.
A print FILE*-ra ír, tehát ugyanabba a
stdout pufferbe kerül, mint a printf, ha a
fwrite útvonalon halad. A std::cout
alapértelmezett sync_with_stdio(true) mellett szintén a
stdout pufferét használja. A sorrend tehát
elvileg konzisztens — a gyakorlatban mégsem mindig az.
Levezetés:
printf kimenete a stdout
FILE pufferébe kerül.'\n'-nél flush); csőben és fájlban blokkolt puffert
használ.print Unicode-képes terminálon a natív API-t
választja, a bájtok közvetlenül a file descriptorra
mennek, megkerülve a FILE puffert.printf nem végződött '\n'-nel,
bájtjai még a pufferben vannak.A javítás nem varázslat, hanem szerződés: vagy egy mechanizmust
használj, vagy a váltás előtt hívd meg a
std::fflush(stdout)-ot. A std::print önmagában
nem flushol; a println új sora csak sorbufferelt terminálon
vált ki flushot.
Teljesítmény. A könyv nem mér; az alábbi táblázat a szerződésekből levezetett költségforrásokat mutatja, nem mért számokat.
| útvonal | formátum-ellenőrzés | köztes allokáció | tipikus költségforrás |
|---|---|---|---|
printf |
nincs | nincs | futásidejű parse, varargs |
std::cout << |
nincs | nincs | elemenkénti hívás, locale, virtualitás |
std::format → std::string |
fordítási idő | igen | parse + string-allokáció |
std::print(FILE*) |
fordítási idő | implementációfüggő | parse + write |
A print a format futásidejű parse-át is
elvégzi (a validáció fordítási idejű, a tényleges előállítás nem),
viszont elkerüli a std::string allokációt, ha közvetlenül
FILE*-ra ír. Az iostream számonkénti
túlterhelése numerikus terhelésnél jellemzően érzékelhető; a
printf és a print viszonya erősen függ a
formátumspecifikációtól és az implementációtól. Bármilyen
összehasonlítást a tanuló saját laborjában, azonos kimenettel, a mérésen
kívüli flushel és std::chrono::steady_clock-kal kell
elvégezni.
std::vformat kell, vagy C++26
runtime_format.print-et ostream-re
hívni. Nincs ilyen túlterhelés; std::format +
operator<<.print flushol. Nem;
terminálon a sorbufferelés ad látszatot.printf és print keverése Unicode
terminálon. A natív útvonal megkerüli a FILE
puffert.{:?}-t minden típusra univerzálisnak
tekinteni. A debug-viselkedés a formatter szerződése.<print> fejléc elhagyása és
printf-re hagyatkozás. A format és a
print két külön eszköz.A std::print és std::println
FILE* célra ír, fmt konstans kifejezés, a
formátum ellenőrzése fordítási idejű. ostream-re nincs
túlterhelés. Terminálon a vprint_unicode, máshol a
vprint_nonunicode (mindenhol fwrite) útvonal
érvényes; a kódolásváltás nem a print dolga. A P2286
range-formázást ad: [...], (...),
{kulcs: érték}, a stringek idézőjelben, a ?
escape-el. A printf-fel való keverés csak flush-sal
determinisztikus. A teljesítményt mérni kell, nem kijelenteni.
1. (kódolás, alap). Írj programot, amely egy
FILE*-ra std::print-tel és
std::fprintf-fel felváltva ír, és a fájl bezárása előtt nem
hív mást. Magyarázd meg, miért determinisztikus a sorrend, és mi
történne, ha stdout terminálra írnál Unicode-képes
környezetben.
2. (kódolás, közepes). Formázz egy
std::map<std::string, std::vector<std::pair<int, int>>>
értéket {} és {:?} specifikátorral. Írd le a
különbséget, és adj egy olyan elemet, ahol a ? láthatóan
változtat.
3. (diagnosztika, haladó). Készíts harness-t, amely
printf, std::cout << és
std::format + std::fwrite útvonalon kiír
egész számot, és steady_clock-kal méri az időt. Sorolj fel
legalább négy érvényességi feltételt, amely nélkül a mérés
félrevezető.
1. megoldás.
FILE* f = std::fopen("out.txt", "w") után a
std::print(f, ...) a fwrite útvonalon halad (a
fájl nem terminál), tehát ugyanabba a FILE pufferbe ír,
mint a fprintf; így a sorrend a hívások sorrendje, és a
std::fclose flushol. stdout-ra Unicode-képes
terminálon a print a natív API-t választhatja, ami
megkerüli a FILE puffert; a korábbi fprintf
bájtjai még a pufferben lehetnek, ezért a látszólagos sorrend
felcserélődhet. Ellenőrzési feltétel: a fopen visszatérési
értékét ellenőrizd, és a fájlt fclose-zal zárd.
2. megoldás.
#include <print>
#include <map>
#include <string>
#include <utility>
#include <vector>
int main() {
std::map<std::string, std::vector<std::pair<int, int>>> m{
{"a", {{1, 2}, {3, 4}}},
{"b", {{5, 6}}}
};
std::println("{}", m);
std::println("{:?}", m);
}A {} a string kulcsokat idézőjelben, a pair-eket
(1, 2) alakban adja; a {:?} a stringekben
escape-eli a nem nyomtatható karaktereket. A ? láthatóan
akkor változtat, ha a kulcs tartalmaz pl. '\n'-t vagy
tabulátort. Ellenőrzési feltétel: a formátumsztring konstans, és a
__cpp_lib_format_ranges definiált.
3. megoldás (vázlat).
#include <chrono>
#include <cstdio>
#include <format>
#include <iostream>
#include <print>
int main() {
constexpr int N = 100000;
std::FILE* f = std::fopen("/dev/null", "w");
if (!f) { std::perror("fopen"); return 1; }
std::setvbuf(f, nullptr, _IOFBF, 1 << 20);
auto t0 = std::chrono::steady_clock::now();
for (int i = 0; i < N; ++i) std::fprintf(f, "%d\n", i);
auto t1 = std::chrono::steady_clock::now();
for (int i = 0; i < N; ++i) std::cout << i << '\n';
auto t2 = std::chrono::steady_clock::now();
for (int i = 0; i < N; ++i) {
auto s = std::format("{}\n", i);
std::fwrite(s.data(), 1, s.size(), f);
}
auto t3 = std::chrono::steady_clock::now();
std::fflush(f);
std::fclose(f);
auto ms = [](auto a, auto b) {
return std::chrono::duration_cast<std::chrono::microseconds>(b - a).count();
};
std::println("printf={}us cout={}us format={}us",
ms(t0, t1), ms(t1, t2), ms(t2, t3));
}Érvényességi feltételek: (a) mindhárom útvonal ugyanazt a
bájtfolyamot állítja elő, és ezt ellenőrizni kell; (b) a
cout és a printf ne ossza ugyanazt a
stdout puffert (itt külön FILE*-ra írunk,
illetve sync_with_stdio(false) kell); (c) a flush a mért
szakaszon kívül történjen, vagy minden útvonalra azonos módon; (d) a
fordító ne optimalizálja ki a ciklust; (e) a mérés ismételt, és a
mediánt nézzük, ne az első futást. A könyv ezt a harness-t nem futtatta,
a várt különbség irányát csak a fenti költségtáblázat indokolja, konkrét
számot nem állítunk.