Vai al contenuto

I/O: Stream, Formattazione e std::print

Padroneggia l'I/O del C++ dal classico iostream al moderno std::format e std::print, inclusi gli stati degli stream, l'I/O su file e le insidie comuni dei buffer.

L'Evoluzione dell'I/O in C++

L'I/O del C++ si è evoluto significativamente nella storia del linguaggio. La classica libreria iostream (std::cin, std::cout) è stata lo standard dal C++98, ma ha problemi noti di ergonomia e prestazioni. Il C++20 ha introdotto std::format, una libreria di formattazione type-safe ispirata al str.format() di Python. Il C++23 ha aggiunto std::print, che combina formattazione e output in una singola chiamata.

Comprendere tutti e tre gli approcci è importante: iostream è ovunque nel codice esistente, std::format è il modo moderno per formattare le stringhe, e std::print è il futuro dell'output C++.

I/O Classico: cout, cin, cerr

La libreria iostream fornisce oggetti stream per l'input standard, l'output standard e l'output degli errori:

io_basics.cppc++23
#include <iostream>
#include <string>
#include <limits>    // For numeric_limits

int main() {
    // Output with cout
    std::cout << "Hello, " << "World!" << '\n';      // Chained insertion
    std::cout << "The answer is: " << 42 << '\n';    // Automatic type formatting

    // Error output — cerr is unbuffered (immediate), clog is buffered
    std::cerr << "Error: something went wrong\n";

    // Input with cin
    std::cout << "Enter your name: ";
    std::string name;
    std::getline(std::cin, name);         // Read entire line (including spaces)

    std::cout << "Enter your age: ";
    int age{};
    std::cin >> age;                       // Read a single value (stops at whitespace)

    // Check if input succeeded
    if (std::cin.fail()) {
        std::cerr << "Invalid input!\n";
        std::cin.clear();                  // Reset error flags
        std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');  // Discard bad input
    }

    // The getline-after-cin trap:
    // After cin >> age, a '\n' remains in the buffer.
    // A subsequent getline() would read an empty string!
    // Fix: call cin.ignore() before getline():
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');

    std::string city;
    std::cout << "Enter your city: ";
    std::getline(std::cin, city);          // Now works correctly

    std::cout << name << " (" << age << ") from " << city << '\n';
}
Output

Premi Esegui per compilare ed eseguire questo esempio.

Flag di Stato degli Stream

Ogni stream mantiene un insieme di flag di stato che indicano la sua condizione attuale:

- goodbit — Nessun errore. Lo stream è pronto per le operazioni di I/O.
- eofbit — È stata raggiunta la fine del file/input. Viene impostato quando lo stream tenta di leggere oltre la fine.
- failbit — Si è verificato un errore logico (es. tentare di leggere un intero ma trovare del testo). Lo stream è ancora utilizzabile dopo aver chiamato clear().
- badbit — Si è verificato un errore di I/O irrecuperabile (es. guasto del disco). Lo stream è tipicamente inutilizzabile.

Controlli con stream.good(), stream.eof(), stream.fail(), stream.bad(), o usando lo stream in un contesto booleano: if (std::cin) è equivalente a if (!std::cin.fail()).

Un pattern critico: controlla sempre lo stato dello stream dopo le operazioni di input, non prima. L'idioma comune while (std::cin >> value) legge e controlla in un solo passo — si ferma quando la lettura fallisce.

Insidia

Il bug di I/O più comune in C++ riguarda il mescolare >> (estrazione formattata) con std::getline(). L'operatore >> legge un valore ma lascia il newline finale nel buffer. Un successivo std::getline() legge poi quel newline residuo come una stringa vuota, sembrando saltare l'input.

La soluzione è chiamare std::cin.ignore(std::numeric_limits::max(), '\n') tra >> e std::getline() per scartare il newline residuo.

Un altro approccio è usare std::getline() per tutto l'input e analizzare le stringhe manualmente con std::istringstream o std::stoi/std::stod. Questo evita completamente il problema e offre una gestione degli errori migliore.

  • Dopo cin >> x, un newline rimane nel buffer — il getline() successivo lo legge come una stringa vuota
  • Soluzione: chiama cin.ignore(numeric_limits::max(), '\n') tra >> e getline()
  • Alternativa: usa getline() per TUTTO l'input e analizza le stringhe con stoi/stod o istringstream
  • Controlla sempre lo stato dello stream dopo l'input: if (cin >> x) piuttosto che leggere alla cieca

I/O su File

L'I/O su file usa la stessa interfaccia stream dell'I/O da console, con std::ifstream per la lettura e std::ofstream per la scrittura:

file_io.cppc++23
#include <fstream>
#include <iostream>
#include <string>
#include <vector>

int main() {
    // Writing to a file
    {
        std::ofstream out("scores.txt");  // Opens for writing (creates or truncates)
        if (!out) {
            std::cerr << "Failed to open file for writing\n";
            return 1;
        }
        out << "Alice 95\n";
        out << "Bob 87\n";
        out << "Charlie 92\n";
    }  // File is automatically closed when 'out' goes out of scope (RAII!)

    // Reading from a file
    std::ifstream in("scores.txt");
    if (!in) {
        std::cerr << "Failed to open file for reading\n";
        return 1;
    }

    std::string name;
    int score{};
    std::vector<std::pair<std::string, int>> results;

    while (in >> name >> score) {           // Read until failure (EOF or bad data)
        results.emplace_back(name, score);
    }

    for (const auto& [n, s] : results) {
        std::cout << n << ": " << s << '\n';
    }

    // Appending to a file
    std::ofstream append("scores.txt", std::ios::app);  // Open in append mode
    append << "Diana 98\n";

    // Reading entire file line by line
    std::ifstream in2("scores.txt");
    std::string line;
    while (std::getline(in2, line)) {
        std::cout << "Line: " << line << '\n';
    }
}
Output

Premi Esegui per compilare ed eseguire questo esempio.

Formattazione Moderna: std::format e std::print

std::format del C++20 e std::print del C++23 forniscono formattazione type-safe in stile Python che è sia più sicura che più leggibile dei manipolatori di iostream:

format_print.cppc++23
#include <format>   // C++20
#include <print>    // C++23
#include <string>
#include <vector>
#include <numbers>  // C++20: mathematical constants

int main() {
    // std::format (C++20) — returns a formatted std::string
    std::string msg = std::format("Hello, {}!", "World");
    // msg == "Hello, World!"

    // Positional arguments
    std::string pos = std::format("{1} comes before {0}", "second", "first");
    // pos == "first comes before second"

    // Formatting specifiers
    std::string nums = std::format(
        "int: {:>10d}\n"    // Right-aligned, width 10, decimal
        "hex: {:#06x}\n"    // '0x' prefix, width 6, zero-padded hex
        "float: {:.3f}\n"   // 3 decimal places, fixed notation
        "sci: {:.2e}\n"     // 2 decimal places, scientific notation
        "pi: {:.10f}",      // 10 decimal places
        42, 255, 3.14159, 12345.6789, std::numbers::pi
    );

    // std::print (C++23) — format + output in one step
    std::print("Name: {}, Age: {}\n", "Alice", 30);
    std::print("Pi is approximately {:.6f}\n", std::numbers::pi);

    // std::println (C++23) — like print but adds a newline
    std::println("Score: {:>5}", 95);       // "Score:    95"
    std::println("Score: {:>5}", 100);      // "Score:   100"

    // Formatting containers — print each element
    std::vector<int> values = {10, 20, 30, 40, 50};
    for (std::size_t i = 0; i < values.size(); ++i) {
        std::println("[{:2}] = {:>4}", i, values[i]);
    }

    // Why prefer std::format/print over iostream?
    // 1. Type-safe (unlike printf)
    // 2. Readable format strings (unlike iostream manipulators)
    // 3. No state leakage (iostream manipulators are sticky!)
    // 4. Better performance in many cases
}
Output

Premi Esegui per compilare ed eseguire questo esempio.

Buona pratica

Il C++ moderno ti offre tre approcci per l'I/O. Ecco quando usare ciascuno:

Usa std::print/std::println (C++23) per il codice nuovo quando il tuo compilatore lo supporta. È la sintassi più pulita, type-safe, e evita i problemi dei manipolatori stateful di iostream.

Usa std::format (C++20) quando hai bisogno di una stringa formattata (non output diretto), o quando il tuo compilatore supporta il C++20 ma non il C++23.

Usa iostream quando interagisci con codice che si aspetta oggetti stream, quando hai bisogno dell'astrazione degli stream (es. lettura da cin, I/O su file), o quando lavori con codebase legacy.

Evita printf/scanf del C — non sono type-safe (passare lo specificatore di formato sbagliato è un comportamento indefinito) e non funzionano con i tipi C++ come std::string.

  • Preferisci std::print/println (C++23) per il nuovo codice di output — più pulito e sicuro
  • Usa std::format (C++20) quando hai bisogno di una stringa formattata, non di output diretto
  • iostream è ancora necessario per cin, I/O su file e astrazioni basate su stream
  • Evita printf/scanf — non sono type-safe e non possono gestire std::string direttamente
Concetti chiave
  • Controlla sempre lo stato dello stream dopo le operazioni di input — usa 'if (cin >> x)' o 'while (getline(in, line))'
  • Mescolare cin >> e getline() causa il bug del newline nel buffer — chiama cin.ignore() tra di essi
  • I file stream usano il RAII — si chiudono automaticamente quando escono dallo scope
  • std::format (C++20) fornisce formattazione di stringhe type-safe in stile Python
  • std::print/println (C++23) combina formattazione e output — il modo preferito per il codice nuovo
  • I manipolatori di iostream sono stateful (persistenti) — std::format evita completamente questo problema

Quiz — Metti alla prova le tue conoscenze

(10 XP)

1. Cosa succede se chiami `std::getline(std::cin, str)` immediatamente dopo `std::cin >> number`?

2. Cosa produce `std::format("{:#06x}", 255)`?

3. Perché i file stream (ifstream/ofstream) si chiudono automaticamente quando escono dallo scope?