Aller au contenu

E/S : flux, formatage et std::print

Maîtrisez les E/S C++ des iostream classiques aux modernes std::format et std::print, incluant les états des flux, les E/S fichier, et les pièges courants de tampon.

L'évolution des E/S en C++

Les E/S en C++ ont considérablement évolué au fil de l'histoire du langage. La bibliothèque classique iostream (std::cin, std::cout) est le standard depuis C++98, mais elle a des problèmes d'ergonomie et de performance bien connus. C++20 a introduit std::format, une bibliothèque de formatage type-safe inspirée de str.format() de Python. C++23 a ajouté std::print, qui combine le formatage et la sortie en un seul appel.

Comprendre les trois approches est important : iostream est partout dans le code existant, std::format est la façon moderne de formater des chaînes, et std::print est l'avenir de la sortie en C++.

E/S classiques : cout, cin, cerr

La bibliothèque iostream fournit des objets de flux pour l'entrée standard, la sortie et la sortie d'erreur :

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';
}
Sortie

Cliquez sur Exécuter pour compiler et lancer cet exemple.

Indicateurs d'état des flux

Chaque flux maintient un ensemble d'indicateurs d'état qui indiquent sa condition actuelle :

- goodbit — Aucune erreur. Le flux est prêt pour les opérations d'E/S.
- eofbit — La fin du fichier/de l'entrée a été atteinte. Activé quand le flux tente de lire au-delà de la fin.
- failbit — Une erreur logique s'est produite (par ex., tenter de lire un entier mais rencontrer du texte). Le flux est encore utilisable après avoir appelé clear().
- badbit — Une erreur d'E/S irrécupérable s'est produite (par ex., défaillance de disque). Le flux est typiquement inutilisable.

Vous les vérifiez avec stream.good(), stream.eof(), stream.fail(), stream.bad(), ou en utilisant le flux dans un contexte booléen : if (std::cin) est équivalent à if (!std::cin.fail()).

Un patron critique : vérifiez toujours l'état du flux après les opérations d'entrée, pas avant. L'idiome courant while (std::cin >> value) lit et vérifie en une seule étape — il s'arrête quand la lecture échoue.

Piège

Le bogue d'E/S le plus courant en C++ implique le mélange de >> (extraction formatée) avec std::getline(). L'opérateur >> lit une valeur mais laisse le retour à la ligne restant dans le tampon. Un appel suivant à std::getline() lit alors ce retour à la ligne résiduel comme une chaîne vide, semblant sauter l'entrée.

La solution est d'appeler std::cin.ignore(std::numeric_limits::max(), '\n') entre >> et std::getline() pour éliminer le retour à la ligne résiduel.

Une autre approche est d'utiliser std::getline() pour toute l'entrée et d'analyser les chaînes vous-même avec std::istringstream ou std::stoi/std::stod. Cela évite complètement le problème et vous donne une meilleure gestion des erreurs.

  • Après cin >> x, un retour à la ligne reste dans le tampon — le prochain getline() le lit comme une chaîne vide
  • Solution : appelez cin.ignore(numeric_limits::max(), '\n') entre >> et getline()
  • Alternative : utilisez getline() pour TOUTE l'entrée et analysez les chaînes avec stoi/stod ou istringstream
  • Vérifiez toujours l'état du flux après l'entrée : if (cin >> x) plutôt que de lire aveuglément

E/S fichier

Les E/S fichier utilisent la même interface de flux que les E/S console, avec std::ifstream pour la lecture et std::ofstream pour l'écriture :

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';
    }
}
Sortie

Cliquez sur Exécuter pour compiler et lancer cet exemple.

Formatage moderne : std::format et std::print

std::format de C++20 et std::print de C++23 fournissent un formatage type-safe, à la Python, qui est à la fois plus sûr et plus lisible que les manipulateurs 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
}
Sortie

Cliquez sur Exécuter pour compiler et lancer cet exemple.

Bonne pratique

Le C++ moderne vous offre trois approches d'E/S. Voici quand utiliser chacune :

Utilisez std::print/std::println (C++23) pour le nouveau code quand votre compilateur le supporte. C'est la syntaxe la plus propre, type-safe, et évite les problèmes de manipulateurs à état d'iostream.

Utilisez std::format (C++20) quand vous avez besoin d'une chaîne formatée (pas de sortie directe), ou quand votre compilateur supporte C++20 mais pas C++23.

Utilisez iostream quand vous interagissez avec du code qui attend des objets de flux, quand vous avez besoin de l'abstraction de flux (par ex., lecture depuis cin, E/S fichier), ou quand vous travaillez avec des bases de code existantes.

Évitez printf/scanf du C — ils ne sont pas type-safe (passer le mauvais spécificateur de format est un comportement indéfini) et ne fonctionnent pas avec les types C++ comme std::string.

  • Préférez std::print/println (C++23) pour le nouveau code de sortie — le plus propre et le plus sûr
  • Utilisez std::format (C++20) quand vous avez besoin d'une chaîne formatée, pas d'une sortie directe
  • iostream est toujours nécessaire pour cin, les E/S fichier, et les abstractions basées sur les flux
  • Évitez printf/scanf — ils ne sont pas type-safe et ne gèrent pas std::string directement
Points clés
  • Vérifiez toujours l'état du flux après les opérations d'entrée — utilisez 'if (cin >> x)' ou 'while (getline(in, line))'
  • Mélanger cin >> et getline() cause le bogue du retour à la ligne dans le tampon — appelez cin.ignore() entre les deux
  • Les flux fichier utilisent le RAII — ils se ferment automatiquement quand ils sortent de portée
  • std::format (C++20) fournit un formatage de chaînes type-safe, à la Python
  • std::print/println (C++23) combine formatage et sortie — la méthode préférée pour le nouveau code
  • Les manipulateurs iostream sont à état (persistants) — std::format évite complètement ce problème

Quiz — Testez vos connaissances

(10 XP)

1. Que se passe-t-il si vous appelez `std::getline(std::cin, str)` immédiatement après `std::cin >> number` ?

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

3. Pourquoi les flux fichier (ifstream/ofstream) se ferment-ils automatiquement quand ils sortent de portée ?