Comment lire un fichier entier dans une chaîne std :: string en C ++?


178

Comment puis-je lire un fichier dans un std::string, c'est-à-dire lire le fichier entier à la fois?

Le mode texte ou binaire doit être spécifié par l'appelant. La solution doit être conforme aux normes, portable et efficace. Il ne doit pas copier inutilement les données de la chaîne et il doit éviter les réallocations de mémoire lors de la lecture de la chaîne.

Une façon de faire serait de statuer la taille du fichier, de redimensionner le std::stringet fread()dans le std::string's const_cast<char*>()' ed data(). Cela nécessite que les std::stringdonnées s soient contiguës, ce qui n'est pas requis par la norme, mais cela semble être le cas pour toutes les implémentations connues. Pire encore, si le fichier est lu en mode texte, la std::stringtaille du fichier peut ne pas être égale à la taille du fichier.

Des solutions entièrement correctes, conformes aux normes et portables pourraient être construites en utilisant std::ifstreamdes rdbuf()dans un std::ostringstreamet de là dans un std::string. Cependant, cela pourrait copier les données de chaîne et / ou réallouer inutilement de la mémoire.

  • Toutes les implémentations de bibliothèques standard pertinentes sont-elles suffisamment intelligentes pour éviter toute surcharge inutile?
  • Y a-t-il une autre façon de le faire?
  • Ai-je manqué une fonction Boost cachée qui fournit déjà la fonctionnalité souhaitée?


void slurp(std::string& data, bool is_binary)

Notez que certaines choses sont encore sous-spécifiées. Par exemple, quel est le codage des caractères du fichier? Allez-vous essayer de détecter automatiquement (ce qui ne fonctionne que dans quelques cas spécifiques)? Allez-vous honorer par exemple les en-têtes XML vous indiquant l'encodage du fichier? Il n'y a pas non plus de "mode texte" ou de "mode binaire" - pensez-vous au FTP?
Jason Cohen le

Le mode texte et le mode binaire sont des hacks spécifiques à MSDOS et Windows qui tentent de contourner le fait que les retours à la ligne sont représentés par deux caractères dans Windows (CR / LF). En mode texte, ils sont traités comme un caractère ('\ n').
Ferruccio le

1
Bien que ce ne soit pas (tout à fait) exactement un doublon, cela est étroitement lié à: comment préallouer de la mémoire pour un objet std :: string? (qui, contrairement à la déclaration de Konrad ci-dessus, comprenait du code pour ce faire, en lisant le fichier directement dans la destination, sans faire une copie supplémentaire).
Jerry Coffin

1
"contigu n'est pas exigé par la norme" - oui, il l'est, de manière détournée. Dès que vous utilisez op [] sur la chaîne, elle doit être fusionnée dans un tampon inscriptible contigu, donc il est garanti d'écrire en toute sécurité dans & str [0] si vous .resize () suffisamment grand d'abord. Et en C ++ 11, la chaîne est simplement toujours contiguë.
Tino Didriksen

2
Lien connexe: Comment lire un fichier en C ++? - benchmark et discute des différentes approches. Et oui, rdbuf(celui de la réponse acceptée) n'est pas le plus rapide read.
legends2k

Réponses:


138

Une façon consiste à vider le tampon de flux dans un flux de mémoire séparé, puis à le convertir en std::string:

std::string slurp(std::ifstream& in) {
    std::ostringstream sstr;
    sstr << in.rdbuf();
    return sstr.str();
}

C'est très concis. Cependant, comme indiqué dans la question, cela effectue une copie redondante et, malheureusement, il n'y a fondamentalement aucun moyen d'éliminer cette copie.

La seule vraie solution qui évite les copies redondantes est de faire la lecture manuellement en boucle, malheureusement. Puisque C ++ a maintenant garanti des chaînes contiguës, on pourrait écrire ce qui suit (≥C ++ 14):

auto read_file(std::string_view path) -> std::string {
    constexpr auto read_size = std::size_t{4096};
    auto stream = std::ifstream{path.data()};
    stream.exceptions(std::ios_base::badbit);

    auto out = std::string{};
    auto buf = std::string(read_size, '\0');
    while (stream.read(& buf[0], read_size)) {
        out.append(buf, 0, stream.gcount());
    }
    out.append(buf, 0, stream.gcount());
    return out;
}

20
Quel est l'intérêt d'en faire un oneliner? J'opterais toujours pour un code lisible. En tant que passionné de VB.Net (IIRC), je pense que vous devriez comprendre le sentiment?
sehe

5
@sehe: Je m'attendrais à ce que tout codeur C ++ à moitié compétent comprenne facilement ce one-liner. C'est assez apprivoisé par rapport à d'autres choses.
DevSolar

43
@DevSolar Eh bien, la version la plus lisible est ~ 30% plus courte, n'a pas de distribution et est par ailleurs équivalente. Ma question se pose donc: "A quoi ça sert d'en faire un oneliner?"
sehe

13
Remarque: cette méthode lit le fichier dans le tampon du flux de chaînes, puis copie ce tampon entier dans le string. C'est à dire nécessitant deux fois plus de mémoire que certaines des autres options. (Il n'y a aucun moyen de déplacer le tampon). Pour un fichier volumineux, ce serait une pénalité importante, pouvant même entraîner un échec d'allocation.
MM

9
@DanNissenbaum Vous confondez quelque chose. La concision est en effet importante dans la programmation, mais la bonne façon d'y parvenir est de décomposer le problème en parties et de les encapsuler en unités indépendantes (fonctions, classes, etc.). L'ajout de fonctions n'enlève rien à la concision; bien au contraire.
Konrad Rudolph

52

Voir cette réponse sur une question similaire.

Pour votre commodité, je republie la solution de CTT:

string readFile2(const string &fileName)
{
    ifstream ifs(fileName.c_str(), ios::in | ios::binary | ios::ate);

    ifstream::pos_type fileSize = ifs.tellg();
    ifs.seekg(0, ios::beg);

    vector<char> bytes(fileSize);
    ifs.read(bytes.data(), fileSize);

    return string(bytes.data(), fileSize);
}

Cette solution a entraîné des temps d'exécution environ 20% plus rapides que les autres réponses présentées ici, en prenant la moyenne de 100 courses contre le texte de Moby Dick (1,3M). Pas mal pour une solution C ++ portable, j'aimerais voir les résultats de la mmap'ing du fichier;)


3
connexe: comparaison des performances temporelles de diverses méthodes: lecture d'un fichier entier à la fois en C ++
jfs

12
Jusqu'à aujourd'hui, je n'ai jamais vu tellg () rapporter des résultats sans taille de fichier. Il m'a fallu des heures pour trouver la source du bogue. Veuillez ne pas utiliser tellg () pour obtenir la taille du fichier. stackoverflow.com/questions/22984956/…
Puzomor Croatie

tu ne devrais pas appeler ifs.seekg(0, ios::end)avant tellg? juste après l'ouverture d'un fichier, le pointeur de lecture est au début et tellgrenvoie donc zéro
Andriy Tylychko

1
vous devez également vérifier les fichiers vides car vous déréférencer nullptrpar&bytes[0]
Andriy Tylychko

ok, j'ai manqué ios::ate, donc je pense qu'une version avec déplacement explicite vers la fin serait plus lisible
Andriy Tylychko

50

La variante la plus courte: Live On Coliru

std::string str(std::istreambuf_iterator<char>{ifs}, {});

Il nécessite l'en-tête <iterator>.

Certains rapports indiquent que cette méthode est plus lente que la pré-allocation de la chaîne et l'utilisation de std::istream::read. Cependant, sur un compilateur moderne avec des optimisations activées, cela ne semble plus être le cas, bien que les performances relatives de diverses méthodes semblent dépendre fortement du compilateur.


7
Pourriez-vous expliquer cette réponse. Dans quelle mesure est-il efficace, lit-il un fichier un caractère à la fois, de toute façon pour préallouer la mémoire?
Martin Beckett

@MM La façon dont j'ai lu cette comparaison, cette méthode est plus lente que la méthode de lecture C ++ pure dans un tampon préalloué.
Konrad Rudolph le

Vous avez raison, c'est un cas où le titre se trouve sous l'exemple de code, plutôt qu'au-dessus :)
MM

@juzzlin C ++ ne fonctionne pas comme ça. Ne pas avoir besoin d'un en-tête dans un environnement spécifique n'est pas une bonne raison pour ne pas l'inclure.
LF

Cette méthode déclenchera-t-elle une réallocation de mémoire plusieurs fois?
pièce cheung

22

Utilisation

#include <iostream>
#include <sstream>
#include <fstream>

int main()
{
  std::ifstream input("file.txt");
  std::stringstream sstr;

  while(input >> sstr.rdbuf());

  std::cout << sstr.str() << std::endl;
}

ou quelque chose de très proche. Je n'ai pas de référence stdlib ouverte pour me vérifier moi-même.

Oui, je comprends que je n'ai pas écrit la slurpfonction comme demandé.


Cela a l'air bien, mais cela ne compile pas. Les changements pour le faire compiler le réduisent à d'autres réponses sur cette page. ideone.com/EyhfWm
JDiMatteo

5
Pourquoi la boucle while?
Zitrax

D'accord. Lorsqu'il operator>>lit dans a std::basic_streambuf, il consommera (ce qui reste de) le flux d'entrée, donc la boucle n'est pas nécessaire.
Remy Lebeau

15

Si vous avez C ++ 17 (std :: filesystem), il existe également cette méthode (qui récupère la taille du fichier au std::filesystem::file_sizelieu de seekget tellg):

#include <filesystem>
#include <fstream>
#include <string>

namespace fs = std::filesystem;

std::string readFile(fs::path path)
{
    // Open the stream to 'lock' the file.
    std::ifstream f(path, std::ios::in | std::ios::binary);

    // Obtain the size of the file.
    const auto sz = fs::file_size(path);

    // Create a buffer.
    std::string result(sz, '\0');

    // Read the whole file into the buffer.
    f.read(result.data(), sz);

    return result;
}

Remarque : vous devrez peut-être utiliser <experimental/filesystem>et std::experimental::filesystemsi votre bibliothèque standard ne prend pas encore entièrement en charge C ++ 17. Vous devrez peut-être également le remplacer result.data()par &result[0]s'il ne prend pas en charge les données std :: basic_string non const .


1
Cela peut provoquer un comportement indéfini; l'ouverture du fichier en mode texte génère un flux différent de celui du fichier disque sur certains systèmes d'exploitation.
MM

1
Développé à l'origine pour boost::filesystemque vous puissiez également utiliser boost si vous n'avez pas de c ++ 17
Gerhard Burger

2
Ouvrir un fichier avec une API et obtenir sa taille avec une autre semble demander des incohérences et des conditions de concurrence.
Arthur Tacca

14

Je n'ai pas assez de réputation pour commenter directement les réponses utilisant tellg().

Veuillez noter que tellg()peut renvoyer -1 en cas d'erreur. Si vous transmettez le résultat de en tellg()tant que paramètre d'allocation, vous devez d'abord vérifier le résultat.

Un exemple du problème:

...
std::streamsize size = file.tellg();
std::vector<char> buffer(size);
...

Dans l'exemple ci-dessus, si tellg()une erreur se produit, il renverra -1. Le transtypage implicite entre signé (c'est-à-dire le résultat de tellg()) et non signé (c'est-à-dire l'argument du vector<char>constructeur) se traduira par une allocation erronée par votre vecteur d'un très grand nombre d'octets. (Probablement 4294967295 octets, soit 4 Go.)

Modification de la réponse de paxos1977 pour tenir compte de ce qui précède:

string readFile2(const string &fileName)
{
    ifstream ifs(fileName.c_str(), ios::in | ios::binary | ios::ate);

    ifstream::pos_type fileSize = ifs.tellg();
    if (fileSize < 0)                             <--- ADDED
        return std::string();                     <--- ADDED

    ifs.seekg(0, ios::beg);

    vector<char> bytes(fileSize);
    ifs.read(&bytes[0], fileSize);

    return string(&bytes[0], fileSize);
}

5

Cette solution ajoute la vérification des erreurs à la méthode basée sur rdbuf ().

std::string file_to_string(const std::string& file_name)
{
    std::ifstream file_stream{file_name};

    if (file_stream.fail())
    {
        // Error opening file.
    }

    std::ostringstream str_stream{};
    file_stream >> str_stream.rdbuf();  // NOT str_stream << file_stream.rdbuf()

    if (file_stream.fail() && !file_stream.eof())
    {
        // Error reading file.
    }

    return str_stream.str();
}

J'ajoute cette réponse car l'ajout de la vérification des erreurs à la méthode d'origine n'est pas aussi trivial que prévu. La méthode d'origine utilise l'opérateur d'insertion de stringstream ( str_stream << file_stream.rdbuf()). Le problème est que cela définit le failbit du flux de chaînes lorsqu'aucun caractère n'est inséré. Cela peut être dû à une erreur ou au fait que le fichier est vide. Si vous recherchez des échecs en inspectant le failbit, vous rencontrerez un faux positif lorsque vous lirez un fichier vide. Comment dissiper toute ambiguïté légitime de l'échec d'insertion de caractères et «échec» d'insérer des caractères parce que le fichier est vide?

Vous pourriez penser à rechercher explicitement un fichier vide, mais c'est plus de code et de vérification d'erreur associée.

La vérification de la condition d'échec str_stream.fail() && !str_stream.eof()ne fonctionne pas, car l'opération d'insertion ne définit pas l'eofbit (sur l'ostringstream ni sur l'ifstream).

Donc, la solution est de changer l'opération. Au lieu d'utiliser l'opérateur d'insertion d'ostringstream (<<), utilisez l'opérateur d'extraction d'ifstream (>>), qui définit l'eofbit. Vérifiez ensuite la condition d'échec file_stream.fail() && !file_stream.eof().

Surtout, lorsque file_stream >> str_stream.rdbuf()rencontre un échec légitime, il ne devrait jamais définir eofbit (selon ma compréhension de la spécification). Cela signifie que la vérification ci-dessus est suffisante pour détecter les échecs légitimes.


3

Quelque chose comme ça ne devrait pas être trop grave:

void slurp(std::string& data, const std::string& filename, bool is_binary)
{
    std::ios_base::openmode openmode = ios::ate | ios::in;
    if (is_binary)
        openmode |= ios::binary;
    ifstream file(filename.c_str(), openmode);
    data.clear();
    data.reserve(file.tellg());
    file.seekg(0, ios::beg);
    data.append(istreambuf_iterator<char>(file.rdbuf()), 
                istreambuf_iterator<char>());
}

L'avantage ici est que nous faisons la réserve en premier afin de ne pas avoir à agrandir la chaîne pendant que nous lisons les choses. L'inconvénient est que nous le faisons char par char. Une version plus intelligente pourrait récupérer tout le buf de lecture, puis appeler underflow.


1
Vous devriez vérifier la version de ce code qui utilise std :: vector pour la lecture initiale plutôt qu'une chaîne. Beaucoup beaucoup plus vite.
paxos1977

3

Voici une version utilisant la nouvelle bibliothèque de système de fichiers avec une vérification d'erreur raisonnablement robuste:

#include <cstdint>
#include <exception>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>

namespace fs = std::filesystem;

std::string loadFile(const char *const name);
std::string loadFile(const std::string &name);

std::string loadFile(const char *const name) {
  fs::path filepath(fs::absolute(fs::path(name)));

  std::uintmax_t fsize;

  if (fs::exists(filepath)) {
    fsize = fs::file_size(filepath);
  } else {
    throw(std::invalid_argument("File not found: " + filepath.string()));
  }

  std::ifstream infile;
  infile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
  try {
    infile.open(filepath.c_str(), std::ios::in | std::ifstream::binary);
  } catch (...) {
    std::throw_with_nested(std::runtime_error("Can't open input file " + filepath.string()));
  }

  std::string fileStr;

  try {
    fileStr.resize(fsize);
  } catch (...) {
    std::stringstream err;
    err << "Can't resize to " << fsize << " bytes";
    std::throw_with_nested(std::runtime_error(err.str()));
  }

  infile.read(fileStr.data(), fsize);
  infile.close();

  return fileStr;
}

std::string loadFile(const std::string &name) { return loadFile(name.c_str()); };

infile.openpeut également accepter std::stringsans convertir avec.c_str()
Matt Eding

filepathn'est pas un std::string, c'est un std::filesystem::path. Il s'avère que std::ifstream::openpeut également accepter l'un de ceux-ci.
David G le

@DavidG, std::filesystem::pathest implicitement convertible enstd::string
Jeffrey Cash le

Selon cppreference.com, la ::openfonction membre sur std::ifstreamqui accepte std::filesystem::pathfonctionne comme si la ::c_str()méthode était appelée sur le chemin. Le sous ::value_type- jacent des chemins est charsous POSIX.
David G le

2

Vous pouvez utiliser la fonction 'std :: getline' et spécifier 'eof' comme délimiteur. Le code qui en résulte est cependant un peu obscur:

std::string data;
std::ifstream in( "test.txt" );
std::getline( in, data, std::string::traits_type::to_char_type( 
                  std::string::traits_type::eof() ) );

5
Je viens de tester cela, cela semble être beaucoup plus lent que d'obtenir la taille du fichier et d'appeler read pour toute la taille du fichier dans un tampon. De l'ordre de 12x plus lent.
David

Cela ne fonctionnera que tant qu'il n'y aura pas de caractères "eof" (par exemple 0x00, 0xff, ...) dans votre fichier. S'il y en a, vous ne lirez qu'une partie du fichier.
Olaf Dietsche

2

N'écrivez jamais dans le tampon const char * de std :: string. Plus jamais! Cela est une énorme erreur.

Réservez () de l'espace pour toute la chaîne de votre std :: string, lisez des morceaux de votre fichier de taille raisonnable dans un tampon et ajoutez-le (). La taille des morceaux dépend de la taille de votre fichier d'entrée. Je suis presque sûr que tous les autres mécanismes portables et compatibles STL feront de même (mais peuvent paraître plus jolis).


5
Depuis C ++ 11, il est garanti que vous pouvez écrire directement dans le std::stringtampon; et je crois que cela a fonctionné correctement sur toutes les implémentations réelles avant cela
MM

1
Depuis C ++ 17, nous avons même non-const std::string::data() méthode pour modifier directement le tampon de chaîne sans recourir à des astuces comme &str[0].
zett42

D'accord avec @ zett42, cette réponse est factuellement incorrecte
jeremyong

0
#include <string>
#include <sstream>

using namespace std;

string GetStreamAsString(const istream& in)
{
    stringstream out;
    out << in.rdbuf();
    return out.str();
}

string GetFileAsString(static string& filePath)
{
    ifstream stream;
    try
    {
        // Set to throw on failure
        stream.exceptions(fstream::failbit | fstream::badbit);
        stream.open(filePath);
    }
    catch (system_error& error)
    {
        cerr << "Failed to open '" << filePath << "'\n" << error.code().message() << endl;
        return "Open fail";
    }

    return GetStreamAsString(stream);
}

usage:

const string logAsString = GetFileAsString(logFilePath);

0

Une fonction mise à jour qui s'appuie sur la solution de CTT:

#include <string>
#include <fstream>
#include <limits>
#include <string_view>
std::string readfile(const std::string_view path, bool binaryMode = true)
{
    std::ios::openmode openmode = std::ios::in;
    if(binaryMode)
    {
        openmode |= std::ios::binary;
    }
    std::ifstream ifs(path.data(), openmode);
    ifs.ignore(std::numeric_limits<std::streamsize>::max());
    std::string data(ifs.gcount(), 0);
    ifs.seekg(0);
    ifs.read(data.data(), data.size());
    return data;
}

Il existe deux différences importantes:

tellg()n'est pas garanti de renvoyer le décalage en octets depuis le début du fichier. Au lieu de cela, comme l'a souligné Puzomor Croatia, il s'agit davantage d'un jeton qui peut être utilisé dans les appels fstream.gcount()cependant le fait revenir la quantité d'octets non formatés dernier extrait. Nous ouvrons donc le fichier, extrayons et supprimons tout son contenu avecignore() pour obtenir la taille du fichier, et construisons la chaîne de sortie en fonction de cela.

Deuxièmement, nous évitons d'avoir à copier les données du fichier d'un std::vector<char>vers unstd::string en écrivant directement dans la chaîne.

En termes de performances, cela devrait être le plus rapide absolu, en allouant la chaîne de taille appropriée à l'avance et en appelant read()une fois. Fait intéressant, utiliser ignore()et countg()au lieu de ateet tellg()sur gcc se compile à peu près à la même chose , petit à petit.


1
Ce code ne fonctionne pas, je reçois une chaîne vide. Je pense que tu voulais ifs.seekg(0)au lieu de ifs.clear()(alors ça marche).
Xeverous

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.