Jak wczytać cały plik do std :: string w C ++?

178

Jak wczytać plik do std::string, tj. Czytać cały plik naraz?

Wzywający powinien określić tryb tekstowy lub binarny. Rozwiązanie powinno być zgodne z normami, przenośne i wydajne. Nie powinien niepotrzebnie kopiować danych ciągu i powinien unikać ponownego przydziału pamięci podczas odczytu ciągu.

Jednym ze sposobów na zrobienie tego może być statystyka rozmiaru pliku, zmiana rozmiaru std::stringi fread()na edycję std::string's const_cast<char*>()' data(). Wymaga std::stringto ciągłości danych, co nie jest wymagane przez standard, ale wydaje się, że tak jest w przypadku wszystkich znanych implementacji. Co gorsza, jeśli plik jest czytany w trybie tekstowym, std::stringrozmiar może nie odpowiadać rozmiarowi pliku.

W pełni poprawne, zgodne ze standardami i przenośne rozwiązania mogą być konstruowane przy użyciu std::ifstreamplików rdbuf()a, std::ostringstreama stamtąd do a std::string. Może to jednak spowodować skopiowanie danych ciągu i / lub niepotrzebną zmianę alokacji pamięci.

  • Czy wszystkie istotne implementacje bibliotek standardowych są wystarczająco inteligentne, aby uniknąć wszystkich niepotrzebnych kosztów ogólnych?
  • Czy jest inny sposób, aby to zrobić?
  • Czy przegapiłem jakąś ukrytą funkcję Boost, która już zapewnia pożądaną funkcjonalność?


void slurp(std::string& data, bool is_binary)
TylerH
źródło
Zwróć uwagę, że nadal masz nieokreślone rzeczy. Na przykład, jakie jest kodowanie znaków w pliku? Czy spróbujesz automatycznie wykryć (co działa tylko w kilku określonych przypadkach)? Czy uszanujesz np. Nagłówki XML informujące o kodowaniu pliku? Nie ma też czegoś takiego jak „tryb tekstowy” czy „tryb binarny” - czy myślisz o FTP?
Jason Cohen
Tryb tekstowy i binarny to specyficzne dla MSDOS i Windows triki, które próbują obejść fakt, że znaki nowej linii są reprezentowane przez dwa znaki w systemie Windows (CR / LF). W trybie tekstowym są traktowane jako jeden znak („\ n”).
Ferruccio,
1
Chociaż nie jest to (całkiem) dokładnie duplikat, jest to ściśle związane z: jak wstępnie przydzielić pamięć dla obiektu std :: string? (który, wbrew powyższej wypowiedzi Konrada, zawierał kod, który to robił, wczytując plik bezpośrednio do miejsca docelowego, bez wykonywania dodatkowej kopii).
Jerry Coffin
1
„norma nie wymaga przyległości” - tak jest, w sposób okrężny. Jak tylko użyjesz op [] na łańcuchu, musi on zostać połączony w ciągły zapisywalny bufor, więc zagwarantowane jest bezpieczne zapisywanie do & str [0], jeśli najpierw .resize () jest wystarczająco duży. W C ++ 11 ciąg jest po prostu zawsze ciągły.
Tino Didriksen
2
Powiązany link: Jak czytać plik w C ++? - porównuje i omawia różne podejścia. I tak, rdbuf(ten w zaakceptowanej odpowiedzi) nie jest najszybszy read.
legends2k

Odpowiedzi:

138

Jednym ze sposobów jest opróżnienie buforu strumienia do osobnego strumienia pamięci, a następnie przekonwertowanie go na std::string:

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

To ładnie zwięzłe. Jednak, jak zauważono w pytaniu, powoduje to utworzenie zbędnej kopii i niestety zasadniczo nie ma możliwości usunięcia tej kopii.

Jedynym prawdziwym rozwiązaniem, które pozwala uniknąć zbędnych kopii, jest niestety ręczne wykonanie odczytu w pętli. Ponieważ C ++ ma teraz gwarantowane ciągłe ciągi, można napisać (≥ 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
źródło
20
Po co robić z tego oneliner? Zawsze wybierałem czytelny kod. Jako samozwańczy entuzjasta VB.Net (IIRC) myślę, że powinieneś zrozumieć ten sentyment?
sehe
5
@sehe: Spodziewałbym się, że każdy w połowie kompetentny programista C ++ z łatwością zrozumie tę jedną linijkę. Jest dość łagodny w porównaniu do innych rzeczy, które są w pobliżu.
DevSolar,
43
@DevSolar Cóż, bardziej czytelna wersja jest ~ 30% krótsza, brakuje jej obsady i poza tym jest równoważna. Dlatego moje pytanie brzmi: „Po co robić z tego oneliner?”
zobaczcie
13
uwaga: ta metoda wczytuje plik do bufora ciągu ciągów, a następnie kopiuje cały bufor do string. To znaczy wymagające dwa razy więcej pamięci niż niektóre inne opcje. (Nie ma możliwości przeniesienia bufora). W przypadku dużego pliku oznaczałoby to znaczną karę, być może nawet powodując błąd alokacji.
MM,
9
@DanNissenbaum Coś mylisz. Zwięzłość jest istotnie ważna w programowaniu, ale właściwym sposobem osiągnięcia tego jest rozbicie problemu na części i zamknięcie ich na niezależne jednostki (funkcje, klasy itp.). Dodawanie funkcji nie umniejsza zwięzłości; wręcz przeciwnie.
Konrad Rudolph
52

Zobacz tę odpowiedź na podobne pytanie.

Dla Twojej wygody ponownie publikuję rozwiązanie 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);
}

To rozwiązanie zaowocowało około 20% szybszym czasem wykonania niż inne przedstawione tutaj odpowiedzi, biorąc pod uwagę średnią ze 100 uruchomień z tekstem Moby Dicka (1,3 mln). Nieźle jak na przenośne rozwiązanie C ++, chciałbym zobaczyć wyniki mmapowania pliku;)

paxos1977
źródło
3
powiązane: porównanie wydajności czasu różnych metod: Odczyt całego pliku naraz w C ++
jfs
12
Aż do dzisiaj, nigdy nie byłem świadkiem raportowania przez tellg () wyników innych niż fileize. Znalezienie źródła błędu zajęło mi wiele godzin. Nie używaj funkcji tellg (), aby uzyskać rozmiar pliku. stackoverflow.com/questions/22984956/…
Puzomor Chorwacja
nie powinieneś ifs.seekg(0, ios::end)wcześniej dzwonić tellg? zaraz po otwarciu pliku wskaźnik odczytu znajduje się na początku i tellgzwraca zero
Andriy Tylychko
1
również trzeba sprawdzić dla pustych plików jak będziesz dereference nullptrprzez&bytes[0]
Andrij Tylychko
ok, przegapiłem ios::ate, więc myślę, że wersja z wyraźnym przesunięciem do końca byłaby bardziej czytelna
Andriy Tylychko
50

Najkrótszy wariant: Live On Coliru

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

Wymaga nagłówka <iterator>.

Pojawiły się doniesienia, że ​​ta metoda jest wolniejsza niż wstępne przydzielanie ciągu i używanie std::istream::read. Jednak w przypadku współczesnego kompilatora z włączoną optymalizacją wydaje się, że nie ma to już miejsca, chociaż względna wydajność różnych metod wydaje się być silnie zależna od kompilatora.

Konrad Rudolph
źródło
7
Czy mógłbyś wyjaśnić tę odpowiedź. Jak skuteczne jest to, czy odczytuje plik po jednym znaku na raz, tak czy inaczej, aby wstępnie przydzielić mieszaną pamięć?
Martin Beckett
@MM Sposób, w jaki czytam to porównanie, jest wolniejszy niż czysta metoda C ++ wczytywania do wstępnie przydzielonego bufora.
Konrad Rudolph
Masz rację, jest to przypadek, w którym tytuł znajduje się pod próbką kodu, a nie nad nim :)
MM
@juzzlin C ++ nie działa w ten sposób. Niewymaganie nagłówka w określonym środowisku nie jest dobrym powodem, aby go nie dołączać.
LF
Czy ta metoda spowoduje wielokrotne ponowne przydzielanie pamięci?
moneta cheung
22

Posługiwać się

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

lub coś bardzo bliskiego. Nie mam otwartego odwołania do biblioteki standardowej, aby sprawdzić się dwukrotnie.

Tak, rozumiem, że nie napisałem slurpfunkcji zgodnie z pytaniem.

Ben Collins
źródło
Wygląda to ładnie, ale się nie kompiluje. Zmiany, które umożliwią kompilację, zredukuj ją do innych odpowiedzi na tej stronie. ideone.com/EyhfWm
JDiMatteo
5
Dlaczego pętla while?
Zitrax
Zgoda. Podczas operator>>wczytywania do a std::basic_streambufzużywa (to, co zostało) strumień wejściowy, więc pętla jest niepotrzebna.
Remy Lebeau
15

Jeśli masz C ++ 17 (std :: filesystem), jest też ten sposób (który pobiera rozmiar pliku std::filesystem::file_sizezamiast seekgi 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;
}

Uwaga : może być konieczne użycie, <experimental/filesystem>a std::experimental::filesystemTwoja standardowa biblioteka nie obsługuje jeszcze w pełni C ++ 17. Może być również konieczne zastąpienie result.data()przez, &result[0]jeśli nie obsługuje danych innych niż stałe std :: basic_string .

Gabriel Majeri
źródło
1
Może to spowodować niezdefiniowane zachowanie; otwarcie pliku w trybie tekstowym daje inny strumień niż plik dyskowy w niektórych systemach operacyjnych.
MM
1
Oryginalnie opracowany boost::filesystemtak, że możesz również użyć boost, jeśli nie masz c ++ 17
Gerhard Burger
2
Otwieranie pliku za pomocą jednego interfejsu API i uzyskiwanie jego rozmiaru za pomocą innego wydaje się prosić o niespójność i warunki wyścigu.
Arthur Tacca
14

Nie mam wystarczającej reputacji, aby bezpośrednio komentować odpowiedzi za pomocą tellg().

Pamiętaj o tym tellg() w przypadku błędu może zwrócić -1. Jeśli przekazujesz wynik tellg()jako parametr alokacji, powinieneś najpierw sprawdzić wynik.

Przykład problemu:

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

W powyższym przykładzie, jeśli tellg()napotka błąd, zwróci -1. Niejawne rzutowanie między znakiem ze znakiem (tj. Wynikiem tellg()) i bez znaku (tj. Argumentem do vector<char>konstruktora) spowoduje, że Twój wektor błędnie przydzieli bardzo dużą liczbę bajtów. (Prawdopodobnie 4294967295 bajtów lub 4 GB.)

Modyfikacja odpowiedzi paxos1977 w celu uwzględnienia powyższego:

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
źródło
5

To rozwiązanie dodaje sprawdzanie błędów do metody opartej na 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();
}

Dodaję tę odpowiedź, ponieważ dodanie sprawdzania błędów do oryginalnej metody nie jest tak trywialne, jak można by się spodziewać. Oryginalna metoda używa operatora wstawiania stringstream (str_stream << file_stream.rdbuf() ). Problem polega na tym, że ustawia to bit failstream łańcucha, gdy nie są wstawiane żadne znaki. Może to być spowodowane błędem lub pustym plikiem. Jeśli sprawdzisz błędy, sprawdzając bit błędów, napotkasz fałszywy alarm podczas odczytu pustego pliku. Jak rozróżnić uzasadniony brak wstawienia jakichkolwiek znaków i „niepowodzenie” wstawienia jakichkolwiek znaków, ponieważ plik jest pusty?

Możesz pomyśleć o jawnym sprawdzeniu pustego pliku, ale to więcej kodu i związanego z nim sprawdzania błędów.

Sprawdzanie stanu awarii str_stream.fail() && !str_stream.eof() nie działa, ponieważ operacja wstawiania nie ustawia eofbita (w strumieniu ostringstream ani w strumieniu ifstream).

Tak więc rozwiązaniem jest zmiana operacji. Zamiast używać operatora wstawiania ostringstream (<<), użyj operatora ekstrakcji ifstream (>>), który ustawia eofbit. Następnie sprawdź stan awarii file_stream.fail() && !file_stream.eof().

Co ważne, gdy file_stream >> str_stream.rdbuf()napotka uzasadnioną awarię, nie powinien nigdy ustawiać eofbit (zgodnie z moim rozumieniem specyfikacji). Oznacza to, że powyższe sprawdzenie jest wystarczające do wykrycia uzasadnionych awarii.

tgnottingham
źródło
3

Coś takiego nie powinno być takie złe:

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

Zaletą jest to, że najpierw robimy rezerwę, więc nie będziemy musieli powiększać łańcucha podczas czytania. Wadą jest to, że robimy to char po znaku. Bardziej inteligentna wersja mogłaby pobrać całą odczytaną wartość bufora, a następnie wywołać niedomiar.

Matt Price
źródło
1
Powinieneś sprawdzić wersję tego kodu, która używa std :: vector do początkowego odczytu zamiast ciągu. Dużo szybciej.
paxos1977
3

Oto wersja korzystająca z nowej biblioteki systemu plików z dość solidnym sprawdzaniem błędów:

#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.
źródło
infile.openmożna też zaakceptować std::stringbez konwersji z.c_str()
Matt Eding
filepathnie jest std::string, to jest std::filesystem::path. Okazuje się, że std::ifstream::openmoże zaakceptować również jedną z nich.
David G
@DavidG, std::filesystem::pathjest niejawnie zamieniany nastd::string
Jeffrey Cash
Według cppreference.com, ::openfunkcja członkowska, std::ifstreamktóra akceptuje, std::filesystem::pathdziała tak, jakby ::c_str()metoda została wywołana na ścieżce. Podstawą ::value_typeścieżek jest charPOSIX.
David G
2

Możesz użyć funkcji „std :: getline” i określić „eof” jako separator. Wynikowy kod jest jednak nieco niejasny:

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
źródło
5
Właśnie to przetestowałem, wydaje się, że jest to znacznie wolniejsze niż pobranie rozmiaru pliku i wywołanie odczytu całego rozmiaru pliku do bufora. Około 12x wolniej.
David
To zadziała tylko tak długo, jak długo w pliku nie będzie żadnych znaków „eof” (np. 0x00, 0xff, ...). Jeśli tak, przeczytasz tylko część pliku.
Olaf Dietsche
2

Nigdy nie zapisuj w buforze const char * std :: string. Nigdy przenigdy! Takie postępowanie jest ogromnym błędem.

Zarezerwuj () miejsce na cały ciąg w swoim std :: string, wczytaj fragmenty pliku o rozsądnym rozmiarze do bufora i dołącz () go. Jak duże muszą być fragmenty, zależy od rozmiaru pliku wejściowego. Jestem prawie pewien, że wszystkie inne przenośne i zgodne z STL mechanizmy zrobią to samo (ale mogą wyglądać ładniej).

Thorsten79
źródło
5
Od C ++ 11 gwarantuje się, że zapisywanie bezpośrednio do std::stringbufora będzie OK ; i uważam, że działało poprawnie na wszystkich wcześniejszych wdrożeniach
MM
1
Od C ++ 17 mamy nawet std::string::data()metodę nie będącą stałą modyfikacją bufora łańcuchowego bezpośrednio, bez uciekania się do takich sztuczek jak &str[0].
zett42
Zgadzam się z @ zett42, ta odpowiedź jest nieprawidłowa
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);
}

stosowanie:

const string logAsString = GetFileAsString(logFilePath);
Paul Sumpner
źródło
0

Zaktualizowana funkcja oparta na rozwiązaniu 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;
}

Istnieją dwie ważne różnice:

tellg()nie gwarantuje zwrócenia przesunięcia w bajtach od początku pliku. Zamiast tego, jak wskazał Puzomor Croatia, jest to raczej token, którego można używać w wywołaniach fstream. gcount()jednak nie zwraca ilość niesformatowanych bajtów ostatni wyodrębnione. Dlatego otwieramy plik, wyodrębniamy i odrzucamy całą jego zawartość za pomocą, ignore()aby uzyskać rozmiar pliku, i na tej podstawie konstruujemy ciąg wyjściowy.

Po drugie, unikamy konieczności kopiowania danych pliku z a std::vector<char>do a std::string, zapisując bezpośrednio do ciągu.

Pod względem wydajności powinno to być absolutnie najszybsze, przydzielając ciąg o odpowiednim rozmiarze z wyprzedzeniem i wywołując read()raz. Ciekawostką jest to, że użycie ignore()i countg()zamiast atei tellg()na gcc kompiluje się do prawie tego samego , krok po kroku.

kiroma
źródło
1
Ten kod nie działa, otrzymuję pusty ciąg. Myślę, że chciałeś ifs.seekg(0)zamiast ifs.clear()(wtedy to działa).
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
źródło
1
Dodaj opis.
Peter
odwiedź i sprawdź jak odpowiedzieć na pytanie .
Yunus Temurlenk