Como leio um arquivo inteiro em uma std :: string em C ++?

178

Como faço para ler um arquivo em um std::string, ou seja, ler o arquivo inteiro de uma só vez?

O modo texto ou binário deve ser especificado pelo chamador. A solução deve ser compatível com os padrões, portátil e eficiente. Ele não deve copiar desnecessariamente os dados da sequência e evitar realocações de memória durante a leitura da sequência.

Uma maneira de fazer isso seria declarar o tamanho do arquivo, redimensionar o std::stringe fread()para o std::string's const_cast<char*>()' ed data(). Isso requer que os std::stringdados sejam contíguos, o que não é exigido pelo padrão, mas parece ser o caso de todas as implementações conhecidas. O que é pior, se o arquivo for lido no modo de texto, o std::stringtamanho do arquivo pode não ser igual ao tamanho do arquivo.

Soluções totalmente corretas, compatíveis com os padrões e portáteis podem ser construídas usando std::ifstream's rdbuf()em ae std::ostringstreamde lá em a std::string. No entanto, isso pode copiar os dados da string e / ou realocar desnecessariamente a memória.

  • Todas as implementações relevantes de bibliotecas padrão são inteligentes o suficiente para evitar toda a sobrecarga desnecessária?
  • tem outro jeito de fazer isto?
  • Perdi alguma função Boost oculta que já fornece a funcionalidade desejada?


void slurp(std::string& data, bool is_binary)
TylerH
fonte
Observe que você ainda tem algumas coisas subespecificadas. Por exemplo, qual é a codificação de caracteres do arquivo? Você tentará detectar automaticamente (que funciona apenas em alguns casos específicos)? Você vai honrar, por exemplo, cabeçalhos XML informando a codificação do arquivo? Também não existe "modo texto" ou "modo binário" - você está pensando em FTP?
Jason Cohen
O modo texto e binário são hacks específicos para MSDOS e Windows que tentam contornar o fato de que as novas linhas são representadas por dois caracteres no Windows (CR / LF). No modo de texto, eles são tratados como um caractere ('\ n').
Ferruccio
1
Embora não seja exatamente uma duplicata, isso está intimamente relacionado a: como pré-alocar memória para um objeto std :: string? (que, ao contrário da declaração de Konrad acima, incluía código para fazer isso, lendo o arquivo diretamente no destino, sem fazer uma cópia extra).
Jerry Coffin
1
"contíguo não é exigido pelo padrão" - sim, é uma maneira indireta. Assim que você usa op [] na string, ela deve ser combinada em um buffer gravável contíguo, para garantir a segurança de gravar em & str [0] se você redimensionar () o suficiente primeiro. E no C ++ 11, a string é simplesmente sempre contígua.
Tino Didriksen
2
Link relacionado: Como ler um arquivo em C ++? - benchmarks e discute as várias abordagens. E sim, rdbuf(aquele na resposta aceita) não é o mais rápido, readé.
legends2k

Respostas:

138

Uma maneira é liberar o buffer de fluxo em um fluxo de memória separado e depois convertê-lo para std::string:

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

Isso é bem conciso. No entanto, conforme observado na pergunta, isso executa uma cópia redundante e, infelizmente, não existe maneira fundamental de excluir essa cópia.

A única solução real que evita cópias redundantes é fazer a leitura manualmente em um loop, infelizmente. Como o C ++ agora tem seqüências de caracteres contíguas garantidas, pode-se escrever o seguinte (≥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
fonte
20
Qual o sentido de torná-lo um oneliner? Eu sempre optaria por código legível. Como um entusiasta autônomo do VB.Net (IIRC), acho que você deve entender o sentimento?
sehe
5
@sehe: Eu esperaria que qualquer codificador C ++ de competência intermediária entendesse prontamente esse código. É bem manso comparado a outras coisas por aí.
DevSolar
43
@DevSolar Bem, a versão mais legível é ~ 30% mais curta, carece de elenco e é equivalente. Minha pergunta, portanto, permanece: "Qual é o sentido de torná-lo oneliner?"
Veja
13
note: esse método lê o arquivo no buffer do stringstream e copia todo o buffer no string. Ou seja, exigindo o dobro de memória que algumas das outras opções. (Não há como mover o buffer). Para um arquivo grande, isso seria uma penalidade significativa, talvez até causando uma falha na alocação.
6166 MM
9
@ DanNissenbaum Você está confundindo algo. A concisão é realmente importante na programação, mas a maneira correta de alcançá-la é decompor o problema em partes e encapsular-o em unidades independentes (funções, classes, etc.). A adição de funções não diminui a concisão; pelo contrário.
21416 Konrad Rudolph
52

Veja esta resposta em uma pergunta semelhante.

Para sua comodidade, estou reposicionando a solução dos 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);
}

Essa solução resultou em tempos de execução cerca de 20% mais rápidos do que as outras respostas apresentadas aqui, ao fazer a média de 100 execuções contra o texto de Moby Dick (1,3M). Nada mal para uma solução portátil C ++, eu gostaria de ver os resultados de mmap'ing do arquivo;)

paxos1977
fonte
3
relacionada: tempo comparação de desempenho de vários métodos: Leitura em um arquivo inteiro de uma vez em C ++
jfs
12
Até hoje, nunca havia testemunhado o tellg () relatando resultados sem tamanho de arquivo. Levei horas para encontrar a fonte do bug. Por favor, não use tellg () para obter o tamanho do arquivo. stackoverflow.com/questions/22984956/…
Puzomor Croatia
você não deveria ligar ifs.seekg(0, ios::end)antes tellg? apenas depois de abrir um arquivo de leitura ponteiro está no início e assim tellgretorna zero
Andriy Tylychko
1
Também é preciso verificar para arquivos vazios como você vai desreferenciava nullptrpor&bytes[0]
Andriy Tylychko
ok, eu perdi ios::ate, então eu acho que uma versão com movimento explícita à final seria mais legível
Andriy Tylychko
50

A variante mais curta: Live On Coliru

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

Requer o cabeçalho <iterator>.

Houve alguns relatos de que esse método é mais lento do que pré-alocar a string e usá-la std::istream::read. No entanto, em um compilador moderno com otimizações ativadas, isso não parece mais ser o caso, embora o desempenho relativo de vários métodos pareça ser altamente dependente do compilador.

Konrad Rudolph
fonte
7
Você poderia explicar essa resposta. Quão eficaz é: ele lê um arquivo de cada vez, para pré-alocar a memória de agitação?
Martin Beckett
@MM A maneira como li essa comparação, esse método é mais lento que o método C ++ puro de leitura em um buffer pré-alocado.
27516 Konrad Rudolph #:
Você está certo, é um caso do ser título sob o exemplo de código, em vez de acima dela :)
MM
@juzzlin C ++ não funciona assim. Não exigir um cabeçalho em um ambiente específico não é um bom motivo para você não incluí-lo.
LF
Esse método acionará a realocação de memória várias vezes?
coin cheung
22

Usar

#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 algo muito próximo. Não tenho uma referência stdlib aberta para me verificar novamente.

Sim, entendo que não escrevi a slurpfunção conforme solicitado.

Ben Collins
fonte
Parece bom, mas não compila. As alterações para compilá-lo reduzem-no a outras respostas nesta página. ideone.com/EyhfWm
JDiMatteo
5
Por que o loop while?
Zitrax 19/10
Acordado. Quando operator>>lê em a std::basic_streambuf, ele consome (o que resta) do fluxo de entrada, portanto o loop é desnecessário.
Remy Lebeau
15

Se você possui C ++ 17 (std :: filesystem), também existe este caminho (que obtém o tamanho do arquivo em std::filesystem::file_sizevez de seekge 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 : pode ser necessário usá-lo <experimental/filesystem>e std::experimental::filesystemse sua biblioteca padrão ainda não suportar totalmente o C ++ 17. Você também pode precisar substituí result.data()- &result[0]lo por se ele não suportar dados não-const std :: basic_string .

Gabriel Majeri
fonte
1
Isso pode causar comportamento indefinido; abrir o arquivo no modo de texto gera um fluxo diferente do arquivo de disco em alguns sistemas operacionais.
MM
1
Originalmente desenvolvido boost::filesystempara que você também possa usar o boost se não tiver c ++ 17
Gerhard Burger
2
Abrir um arquivo com uma API e obter seu tamanho com outra parece exigir inconsistência e condições de corrida.
Arthur Tacca 24/10
14

Eu não tenho reputação suficiente para comentar diretamente sobre as respostas usando tellg().

Esteja ciente de que tellg()pode retornar -1 em caso de erro. Se você estiver passando o resultado tellg()como um parâmetro de alocação, verifique primeiro o sanidade.

Um exemplo do problema:

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

No exemplo acima, se tellg()encontrar um erro, ele retornará -1. A conversão implícita entre assinado (ou seja, o resultado de tellg()) e não assinado (ou seja, o argumento para o vector<char>construtor) resultará em um vetor que aloca erroneamente um número muito grande de bytes. (Provavelmente 4294967295 bytes ou 4 GB.)

Modificando a resposta de paxos1977 para explicar o que foi dito acima:

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
fonte
5

Esta solução adiciona a verificação de erros ao método baseado em 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();
}

Estou adicionando esta resposta porque adicionar verificação de erros ao método original não é tão trivial quanto você esperaria. O método original usa o operador de inserção do stringstream ( str_stream << file_stream.rdbuf()). O problema é que isso define o failbit do stringstream quando nenhum caractere é inserido. Isso pode ocorrer devido a um erro ou ao arquivo estar vazio. Se você verificar falhas ao inspecionar o failbit, encontrará um falso positivo ao ler um arquivo vazio. Como você desambigua a falha legítima em inserir caracteres e a "falha" em inserir caracteres porque o arquivo está vazio?

Você pode procurar explicitamente um arquivo vazio, mas isso significa mais código e verificação de erro associada.

A verificação da condição de falha str_stream.fail() && !str_stream.eof()não funciona, porque a operação de inserção não define o eofbit (no ostringstream nem no ifstream).

Então, a solução é mudar a operação. Em vez de usar o operador de inserção do ostringstream (<<), use o operador de extração do ifstream (>>), que define o eofbit. Em seguida, verifique a condição de falha file_stream.fail() && !file_stream.eof().

É importante file_stream >> str_stream.rdbuf()ressaltar que , quando encontra uma falha legítima, ele nunca deve definir o eofbit (de acordo com meu entendimento da especificação). Isso significa que a verificação acima é suficiente para detectar falhas legítimas.

Tgnottingham
fonte
3

Algo assim não deve ser tão ruim:

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

A vantagem aqui é que fazemos a reserva primeiro, para que não tenhamos que crescer a string enquanto lemos as coisas. A desvantagem é que fazemos char a char. Uma versão mais inteligente pode pegar todo o buf de leitura e, em seguida, chamar underflow.

Matt Price
fonte
1
Você deve fazer o checkout da versão desse código que usa std :: vector para a leitura inicial, em vez de uma string. Muito, muito mais rápido.
22420 paxos1977
3

Aqui está uma versão usando a nova biblioteca de sistemas de arquivos com verificação de erro razoavelmente 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
fonte
infile.opentambém pode aceitar std::stringsem converter com.c_str()
Matt Eding
filepathnão é um std::string, é um std::filesystem::path. Acontece que std::ifstream::openpode aceitar um desses também.
David G
@DavidG, std::filesystem::pathé implicitamente conversível emstd::string
Jeffrey Cash
De acordo com cppreference.com, a ::openfunção de membro std::ifstreamque aceita std::filesystem::pathopera como se o ::c_str()método fosse chamado no caminho. O subjacente ::value_typedos caminhos está charno POSIX.
David G
2

Você pode usar a função 'std :: getline' e especificar 'eof' como delimitador. O código resultante é um pouco obscuro:

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
fonte
5
Acabei de testar isso, parece ser muito mais lento do que obter o tamanho do arquivo e chamar a leitura para todo o tamanho do arquivo em um buffer. Da ordem de 12x mais lento.
David
Isso só funcionará, desde que não haja caracteres "eof" (por exemplo, 0x00, 0xff, ...) em seu arquivo. Se houver, você lerá apenas parte do arquivo.
Olaf Dietsche
2

Nunca escreva no buffer const char * do std :: string. Jamais! Fazer isso é um erro enorme.

Reserve () espaço para toda a cadeia de caracteres em seu std :: string, leia trechos do seu arquivo de tamanho razoável em um buffer e inclua (). O tamanho dos pedaços depende do tamanho do arquivo de entrada. Tenho certeza de que todos os outros mecanismos portáteis e compatíveis com STL farão o mesmo (mas podem parecer mais bonitos).

Thorsten79
fonte
5
Desde o C ++ 11, é garantido que seja bom gravar diretamente no std::stringbuffer; e acredito que funcionou corretamente em todas as implementações reais anteriores a isso #
MM
1
Desde o C ++ 17, temos até um std::string::data()método não-const para modificar o buffer de strings diretamente, sem recorrer a truques como &str[0].
zett42
Concordado com @ zett42, esta resposta está incorreta
jeremyong 15/03
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
fonte
0

Uma função atualizada que se baseia na solução dos 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;
}

Existem duas diferenças importantes:

tellg()Não é garantido o retorno do deslocamento em bytes desde o início do arquivo. Em vez disso, como Puzomor Croatia apontou, é mais um token que pode ser usado nas chamadas fstream. gcount()no entanto , retorna a quantidade de bytes não formatados extraídos pela última vez. Portanto, abrimos o arquivo, extraímos e descartamos todo o seu conteúdo ignore()para obter o tamanho do arquivo e construímos a string de saída com base nisso.

Em segundo lugar, evitamos ter que copiar os dados do arquivo de a std::vector<char>para a std::stringescrevendo diretamente na string.

Em termos de desempenho, esse deve ser o mais rápido possível, alocando a sequência de tamanho apropriada com antecedência e ligando read()uma vez. Como um fato interessante, usar ignore()e em countg()vez de atee tellg()no gcc compila quase a mesma coisa , pouco a pouco.

kiroma
fonte
1
Este código não funciona, estou recebendo uma string vazia. Eu acho que você queria, em ifs.seekg(0)vez de ifs.clear()(então funciona).
Xeverous
-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
fonte
1
Por favor, adicione a descrição.
Peter
visite e veja como responder a uma pergunta .
Yunus Temurlenk