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)

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

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


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.


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.


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 .


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

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.


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.


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

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

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).


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

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.


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

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.