¿Cómo leo un archivo completo en una cadena std :: en C ++?

178

¿Cómo leo un archivo en un archivo std::string, es decir, leo todo el archivo a la vez?

El llamador debe especificar el modo de texto o binario. La solución debe cumplir con los estándares, ser portátil y eficiente. No debe copiar innecesariamente los datos de la cadena, y debe evitar reasignaciones de memoria mientras lee la cadena.

Una forma de hacer esto sería crear el tamaño del archivo, cambiar el tamaño std::stringy fread()el std::string's const_cast<char*>()' ed data(). Esto requiere que los std::stringdatos sean contiguos, lo cual no es requerido por el estándar, pero parece ser el caso para todas las implementaciones conocidas. Lo que es peor, si el archivo se lee en modo texto, el std::stringtamaño del archivo puede no ser igual al tamaño del archivo.

Se podrían construir soluciones portátiles totalmente correctas, que cumplan con los estándares, utilizando std::ifstream's rdbuf()en ay std::ostringstreamdesde allí en a std::string. Sin embargo, esto podría copiar los datos de la cadena y / o reasignar innecesariamente la memoria.

  • ¿Son todas las implementaciones de biblioteca estándar relevantes lo suficientemente inteligentes como para evitar toda sobrecarga innecesaria?
  • ¿Hay otra forma de hacerlo?
  • ¿Perdí alguna función oculta de Boost que ya proporciona la funcionalidad deseada?


void slurp(std::string& data, bool is_binary)
TylerH
fuente
Tenga en cuenta que todavía tiene algunas cosas subespecificadas. Por ejemplo, ¿cuál es la codificación de caracteres del archivo? ¿Intentará detectar automáticamente (que funciona solo en algunos casos específicos)? ¿Honrará, por ejemplo, los encabezados XML que le indican la codificación del archivo? Además, no existe el "modo de texto" o el "modo binario": ¿está pensando en FTP?
Jason Cohen el
El modo de texto y binario son hacks específicos de MSDOS y Windows que intentan evitar el hecho de que las nuevas líneas están representadas por dos caracteres en Windows (CR / LF). En modo texto, se tratan como un carácter ('\ n').
Ferruccio
1
Aunque no es (exactamente) un duplicado exacto, esto está estrechamente relacionado con: ¿cómo asignar previamente la memoria para un objeto std :: string? (que, en contra de la declaración anterior de Konrad, incluía código para hacer esto, leyendo el archivo directamente en el destino, sin hacer una copia adicional).
Jerry Coffin
1
"contiguo no es requerido por el estándar" - sí lo es, de forma indirecta. Tan pronto como utilice op [] en la cadena, debe fusionarse en un búfer de escritura contiguo, por lo que es seguro escribir en & str [0] si .resize () es lo suficientemente grande primero. Y en C ++ 11, la cadena simplemente siempre es contigua.
Tino Didriksen
2
Enlace relacionado: ¿Cómo leer un archivo en C ++? - compara y discute los diversos enfoques. Y sí, rdbuf(el que está en la respuesta aceptada) no es el más rápido read.
legends2k

Respuestas:

138

Una forma es vaciar el búfer de flujo en un flujo de memoria separado y luego convertirlo a std::string:

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

Esto es muy conciso. Sin embargo, como se señaló en la pregunta, esto realiza una copia redundante y desafortunadamente no hay forma fundamental de eludir esta copia.

Lamentablemente, la única solución real que evita las copias redundantes es hacer la lectura manualmente en un bucle. Dado que C ++ ahora tiene cadenas contiguas garantizadas, se podría escribir lo siguiente (≥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;
}
Konrad Rudolph
fuente
20
¿Cuál es el punto de convertirlo en una línea? Siempre optaría por un código legible. Como un entusiasta autoproclamado de VB.Net (IIRC), creo que deberías entender el sentimiento.
sehe
55
@sehe: esperaría que cualquier codificador de C ++ medio competente comprenda fácilmente esa frase. Es bastante manso en comparación con otras cosas que existen.
DevSolar
43
@DevSolar Bueno, la versión más legible es ~ 30% más corta, carece de un yeso y es equivalente. Por lo tanto, mi pregunta es: "¿Cuál es el punto de convertirlo en una línea?"
sehe
13
nota: este método lee el archivo en el búfer del stringstream, luego copia todo el búfer en el string. Es decir, requiere el doble de memoria que algunas de las otras opciones. (No hay forma de mover el búfer). Para un archivo grande, esto sería una penalización significativa, quizás incluso causando una falla de asignación.
MM
9
@DanNissenbaum Estás confundiendo algo. La concisión es realmente importante en la programación, pero la forma correcta de lograrla es descomponer el problema en partes y encapsularlas en unidades independientes (funciones, clases, etc.). Agregar funciones no resta valor a la concisión; Todo lo contrario.
Konrad Rudolph
52

Vea esta respuesta en una pregunta similar.

Para su comodidad, estoy volviendo a publicar la solución 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);
}

Esta solución resultó en tiempos de ejecución aproximadamente un 20% más rápidos que las otras respuestas presentadas aquí, al tomar el promedio de 100 ejecuciones contra el texto de Moby Dick (1.3M). No está mal para una solución portátil de C ++, me gustaría ver los resultados de mmap'ing el archivo;)

paxos1977
fuente
3
relacionado: comparación de rendimiento de tiempo de varios métodos: Lectura en un archivo completo a la vez en C ++
jfs
12
Hasta hoy, nunca he presenciado tellg () informando resultados que no sean de tamaño de archivo. Me tomó horas encontrar la fuente del error. No utilice tellg () para obtener el tamaño del archivo. stackoverflow.com/questions/22984956/…
Puzomor Croacia
no deberías llamar ifs.seekg(0, ios::end)antes tellg? justo después de abrir un archivo, el puntero de lectura está al principio y, por lo tanto, tellgdevuelve cero
Andriy Tylychko
1
también debe verificar si hay archivos vacíos, ya que desreferenciará nullptrpor&bytes[0]
Andriy Tylychko
ok, me lo perdí ios::ate, así que creo que una versión con movimiento explícito hasta el final sería más legible
Andriy Tylychko
50

La variante más corta: Live On Coliru

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

Requiere el encabezado <iterator>.

Hubo algunos informes de que este método es más lento que preasignar la cadena y usarla std::istream::read. Sin embargo, en un compilador moderno con optimizaciones habilitadas, este ya no parece ser el caso, aunque el rendimiento relativo de varios métodos parece ser altamente dependiente del compilador.

Konrad Rudolph
fuente
77
¿Podría explicar esta respuesta? ¿Qué tan eficiente es, lee un archivo a la vez, de todos modos para preasignar la memoria de agitación?
Martin Beckett
@MM La forma en que leo esa comparación, este método es más lento que el método puro de lectura de C ++ en un búfer preasignado.
Konrad Rudolph
Tienes razón, es un caso del ser título bajo el código de ejemplo, en lugar de por encima de ella :)
MM
@juzzlin C ++ no funciona así. No requerir un encabezado en un entorno específico no es una buena razón para no incluirlo.
LF
¿Este método activará la reasignación de memoria muchas veces?
moneda cheung
22

Utilizar

#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;
}

o algo muy cercano. No tengo una referencia stdlib abierta para verificar dos veces.

Sí, entiendo que no escribí la slurpfunción como se me pidió.

Ben Collins
fuente
Esto se ve bien, pero no se compila. Los cambios para que se compile lo reducen a otras respuestas en esta página. ideone.com/EyhfWm
JDiMatteo
55
¿Por qué el bucle while?
Zitrax
Convenido. Cuando se operator>>lee en a std::basic_streambuf, consumirá (lo que queda de) el flujo de entrada, por lo que el bucle es innecesario.
Remy Lebeau
15

Si tiene C ++ 17 (std :: filesystem), también existe de esta manera (que obtiene el tamaño del archivo en std::filesystem::file_sizelugar de seekgy 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;
}

Nota : es posible que necesite usar <experimental/filesystem>y std::experimental::filesystemsi su biblioteca estándar aún no es totalmente compatible con C ++ 17. También podría ser necesario reemplazar result.data()con &result[0]si no soporta std :: basic_string datos no const .

Gabriel Majeri
fuente
1
Esto puede causar un comportamiento indefinido; abrir el archivo en modo de texto produce una secuencia diferente que el archivo de disco en algunos sistemas operativos.
MM
1
Desarrollado originalmente boost::filesystempara que también puedas usar boost si no tienes c ++ 17
Gerhard Burger
2
Abrir un archivo con una API y obtener su tamaño con otra parece estar pidiendo inconsistencia y condiciones de carrera.
Arthur Tacca
14

No tengo suficiente reputación para comentar directamente sobre las respuestas que utilizo tellg().

Tenga en cuenta que tellg()puede devolver -1 en caso de error. Si pasa el resultado tellg()como un parámetro de asignación, primero debe verificar el resultado.

Un ejemplo del problema:

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

En el ejemplo anterior, si tellg() encuentra un error, devolverá -1. La conversión implícita entre firmado (es decir, el resultado de tellg()) y sin signo (es decir, el arg al vector<char>constructor) se traducirá en un su vector erróneamente la asignación de una muy gran número de bytes. (Probablemente 4294967295 bytes, o 4GB.)

Modificando la respuesta de paxos1977 para tener en cuenta lo anterior:

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);
}
Rick Ramstetter
fuente
5

Esta solución agrega verificación de errores al método basado en 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();
}

Estoy agregando esta respuesta porque agregar la comprobación de errores al método original no es tan trivial como cabría esperar. El método original utiliza el operador de inserción de stringstream (str_stream << file_stream.rdbuf() ). El problema es que esto establece el bitbit de secuencia de cadena cuando no se insertan caracteres. Eso puede ser debido a un error o puede ser debido a que el archivo está vacío. Si verifica si hay fallas al inspeccionar el bit de falla, encontrará un falso positivo cuando lea un archivo vacío. ¿Cómo desambigua la falla legítima para insertar caracteres y la "falla" para insertar caracteres porque el archivo está vacío?

Puede pensar en buscar explícitamente un archivo vacío, pero eso es más código y verificación de errores asociados.

La comprobación de la condición de falla str_stream.fail() && !str_stream.eof()no funciona, porque la operación de inserción no establece el eofbit (en el ostringstream ni en el ifstream).

Entonces, la solución es cambiar la operación. En lugar de usar el operador de inserción de ostringstream (<<), use el operador de extracción de ifstream (>>), que establece el eofbit. Luego verifique la condición de falla file_stream.fail() && !file_stream.eof().

Es importante destacar que, cuando se file_stream >> str_stream.rdbuf()encuentra con una falla legítima, nunca debería establecer eofbit (de acuerdo con mi comprensión de la especificación). Eso significa que la verificación anterior es suficiente para detectar fallas legítimas.

tgnottingham
fuente
3

Algo como esto no debería ser tan malo:

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>());
}

La ventaja aquí es que hacemos la reserva primero para no tener que hacer crecer la cadena mientras leemos las cosas. La desventaja es que lo hacemos char por char. Una versión más inteligente podría tomar todo el buf de lectura y luego llamar al flujo inferior.

Matt Price
fuente
1
Debe verificar la versión de este código que usa std :: vector para la lectura inicial en lugar de una cadena. Mucho, mucho más rápido.
paxos1977
3

Aquí hay una versión que usa la nueva biblioteca del sistema de archivos con una comprobación de errores razonablemente robusta:

#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()); };
David G
fuente
infile.opentambién puede aceptar std::stringsin convertir con.c_str()
Matt Eding
filepathno es un std::string, es un std::filesystem::path. Resulta que std::ifstream::openpuede aceptar uno de esos también.
David G
@DavidG, std::filesystem::pathes implícitamente convertible astd::string
Jeffrey Cash
De acuerdo con cppreference.com, la ::openfunción miembro std::ifstreamque acepta std::filesystem::pathopera como si el ::c_str()método fuera llamado en la ruta. El subyacente ::value_typede las rutas está charbajo POSIX.
David G
2

Puede usar la función 'std :: getline' y especificar 'eof' como delimitador. Sin embargo, el código resultante es un poco oscuro:

std::string data;
std::ifstream in( "test.txt" );
std::getline( in, data, std::string::traits_type::to_char_type( 
                  std::string::traits_type::eof() ) );
Martin Cote
fuente
55
Acabo de probar esto, parece ser mucho más lento que obtener el tamaño del archivo y llamar a leer para todo el tamaño del archivo en un búfer. Del orden de 12x más lento.
David
Esto solo funcionará, siempre que no haya caracteres "eof" (por ejemplo, 0x00, 0xff, ...) en su archivo. Si lo hay, solo leerá parte del archivo.
Olaf Dietsche
2

Nunca escriba en el buffer stst :: string's const char *. ¡Nunca jamás! Hacerlo es un gran error.

Reserve () espacio para toda la cadena en su std :: string, lea fragmentos de su archivo de tamaño razonable en un búfer y añádalo (). El tamaño que deben tener los fragmentos depende del tamaño del archivo de entrada. Estoy bastante seguro de que todos los demás mecanismos portátiles y compatibles con STL harán lo mismo (aunque pueden parecer más bonitos).

Thorsten79
fuente
55
Desde C ++ 11 se garantiza que está bien escribir directamente en el std::stringbúfer; y creo que funcionó correctamente en todas las implementaciones reales antes de eso
MM
1
Desde C ++ 17 incluso tenemos un std::string::data()método no constante para modificar el búfer de cadenas directamente sin recurrir a trucos como &str[0].
zett42
De acuerdo con @ zett42 esta respuesta es objetivamente incorrecta
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);
}

uso:

const string logAsString = GetFileAsString(logFilePath);
Paul Sumpner
fuente
0

Una función actualizada que se basa en la solución 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;
}

Hay dos diferencias importantes:

tellg()no se garantiza que devuelva el desplazamiento en bytes desde el comienzo del archivo. En cambio, como señaló Puzomor Croacia, es más un token que se puede usar dentro de las llamadas de fstream. gcount()sin embargo , devuelve la cantidad de bytes sin formato extraídos por última vez. Por lo tanto, abrimos el archivo, extraemos y descartamos todo su contenido conignore() para obtener el tamaño del archivo, y construimos la cadena de salida en función de eso.

En segundo lugar, evitamos tener que copiar los datos del archivo de un std::vector<char>a unstd::string escribiendo directamente en la cadena.

En términos de rendimiento, este debería ser el más rápido absoluto, asignando la cadena del tamaño apropiado con anticipación y llamando read()una vez. Como dato interesante, usar ignore()y en countg()lugar de atey tellg()en gcc se compila casi a la misma cosa , poco a poco.

kiroma
fuente
1
Este código no funciona, estoy obteniendo una cadena vacía. Creo que querías en ifs.seekg(0)lugar de ifs.clear()(entonces funciona).
Xeverous hace
-1
#include <iostream>
#include <fstream>
#include <string.h>
using namespace std;
main(){
    fstream file;
    file.open("test.txt");
    string copy,temp;
    while(getline(file,temp)){
        copy+=temp;
        copy+="\n";
    }
    cout<<copy;
    file.close();
}
Mashaim Tahir
fuente
1
Por favor agregue la descripción.
Peter
Visite y verifique cómo responder una pregunta .
Yunus Temurlenk