41.3 std::print, std::println és tartományok formázása

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.

41.3.1 A print és println szerződése

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++2b

Ha 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.

41.3.2 Unicode kimenet és a két vprint függvény

A std::print mögötti két alacsony szintű függvény:

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.

41.3.3 Range, tuple és tároló formázása (P2286)

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.cpp

A 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.

41.3.4 Pufferelés, kevert kimenet és teljesítmény

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:

  1. A printf kimenete a stdout FILE pufferébe kerül.
  2. Terminálon a glibc alapértelmezésben sorbufferel ('\n'-nél flush); csőben és fájlban blokkolt puffert használ.
  3. Ha a 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.
  4. Ha az előző printf nem végződött '\n'-nel, bájtjai még a pufferben vannak.
  5. A natív írás most kimegy, a puffertartalom csak később → a megjelenő sorrend megfordul.

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.

41.3.5 Gyakori hibák

41.3.6 Összefoglaló

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.

41.3.7 Feladatok

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 NN 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ő.

41.3.8 Megoldások

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.