Bagaimana cara membaca seluruh file menjadi std :: string dalam C ++?

178

Bagaimana cara membaca file menjadi std::string, yaitu, membaca seluruh file sekaligus?

Mode teks atau biner harus ditentukan oleh pemanggil. Solusinya harus sesuai standar, portabel dan efisien. Seharusnya tidak perlu menyalin data string, dan harus menghindari realokasi memori saat membaca string.

Salah satu cara untuk melakukan ini adalah dengan stat filesize, mengubah ukuran std::stringdan fread()ke std::string's const_cast<char*>()' ed data(). Ini membutuhkan std::stringdata yang berdekatan yang tidak diperlukan oleh standar, tetapi tampaknya menjadi kasus untuk semua implementasi yang diketahui. Yang lebih parah, jika file dibaca dalam mode teks, std::stringukurannya mungkin tidak sama dengan ukuran file.

Solusi yang sepenuhnya benar, sesuai standar dan portabel dapat dibangun menggunakan std::ifstream's rdbuf()menjadi std::ostringstreamdan dari sana menjadi a std::string. Namun, ini dapat menyalin data string dan / atau mengalokasikan kembali memori yang tidak perlu.

  • Apakah semua implementasi perpustakaan standar yang relevan cukup pintar untuk menghindari semua overhead yang tidak perlu?
  • Apakah ada cara lain untuk melakukannya?
  • Apakah saya melewatkan beberapa fungsi Peningkatan tersembunyi yang sudah menyediakan fungsi yang diinginkan?


void slurp(std::string& data, bool is_binary)
TylerH
sumber
Perhatikan bahwa Anda masih memiliki beberapa hal yang tidak ditentukan secara spesifik. Misalnya, apa pengkodean karakter file? Apakah Anda akan mencoba deteksi otomatis (yang hanya berfungsi dalam beberapa kasus tertentu)? Apakah Anda menghormati eg Header XML yang memberi tahu Anda penyandian file? Juga tidak ada yang namanya "mode teks" atau "mode biner" - apakah Anda berpikir tentang FTP?
Jason Cohen
Mode teks dan biner adalah peretas khusus MSDOS & Windows yang mencoba menyiasati fakta bahwa baris baru diwakili oleh dua karakter di Windows (CR / LF). Dalam mode teks, mereka diperlakukan sebagai satu karakter ('\ n').
Ferruccio
1
Meskipun bukan duplikat yang persis, ini terkait erat dengan: bagaimana cara pra-mengalokasikan memori untuk objek std :: string? (yang, bertentangan dengan pernyataan Konrad di atas, termasuk kode untuk melakukan ini, membaca file langsung ke tujuan, tanpa melakukan salinan tambahan).
Jerry Coffin
1
"berdekatan tidak diperlukan oleh standar" - ya itu, secara tidak langsung. Segera setelah Anda menggunakan op [] pada string, itu harus digabung menjadi buffer yang dapat ditulisi yang berdekatan, sehingga dijamin aman untuk menulis ke & str [0] jika Anda. Resize () cukup besar (dulu). Dan di C ++ 11, string selalu berdekatan.
Tino Didriksen
2
Tautan terkait: Bagaimana cara membaca file dalam C ++? - benchmark dan membahas berbagai pendekatan. Dan ya, rdbuf(yang ada di jawaban yang diterima) bukan yang tercepat, readadalah.
legends2k

Jawaban:

138

Salah satu caranya adalah menyiram buffer aliran ke dalam aliran memori yang terpisah, dan kemudian mengubahnya menjadi std::string:

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

Ini sangat ringkas. Namun, seperti disebutkan dalam pertanyaan ini melakukan salinan yang berlebihan dan sayangnya pada dasarnya tidak ada cara untuk menghilangkan salinan ini.

Satu-satunya solusi nyata yang menghindari salinan berlebihan adalah dengan melakukan pembacaan secara manual dalam satu lingkaran, sayangnya. Karena C ++ sekarang telah menjamin string yang berdekatan, orang dapat menulis yang berikut ini (≥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
sumber
20
Apa gunanya menjadikannya oneliner? Saya selalu memilih kode yang dapat dibaca. Sebagai seorang penggemar VB.Net yang mengaku diri sendiri (self-mengaku VB.Net) saya pikir Anda harus memahami sentimen?
lihat
5
@ Sehe: Saya harapkan pembuat kode C ++ setengah kompeten siap memahami satu-liner itu. Itu sangat jinak dibandingkan dengan hal-hal lain yang ada di sekitar.
DevSolar
43
@DevSolar Nah, versi yang lebih mudah dibaca ~ 30% lebih pendek, tidak memiliki gips dan sebaliknya setara. Karena itu pertanyaan saya adalah: "Apa gunanya menjadikannya oneliner?"
lihat
13
catatan: metode ini membaca file ke buffer stringstream, lalu menyalin seluruh buffer itu ke dalam string. Yaitu membutuhkan memori dua kali lebih banyak dari beberapa opsi lainnya. (Tidak ada cara untuk memindahkan buffer). Untuk file besar ini akan menjadi penalti yang signifikan, bahkan mungkin menyebabkan kegagalan alokasi.
MM
9
@DanNissenbaum Anda membingungkan sesuatu. Keringkasan memang penting dalam pemrograman, tetapi cara yang tepat untuk mencapainya adalah dengan menguraikan masalah menjadi beberapa bagian dan merangkumnya menjadi unit-unit independen (fungsi, kelas, dll). Menambahkan fungsi tidak mengurangi keringkasan; justru sebaliknya.
Konrad Rudolph
52

Lihat jawaban ini pada pertanyaan serupa.

Demi kenyamanan Anda, saya memposting ulang solusi 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);
}

Solusi ini menghasilkan sekitar 20% waktu eksekusi yang lebih cepat daripada jawaban lain yang disajikan di sini, ketika mengambil rata-rata 100 run terhadap teks Moby Dick (1.3M). Tidak buruk untuk solusi C ++ portabel, saya ingin melihat hasil dari mmap'ing file;)

paxos1977
sumber
3
terkait: perbandingan kinerja waktu dari berbagai metode: Membaca dalam satu file sekaligus dalam C ++
jfs
12
Hingga hari ini, saya tidak pernah menyaksikan tellg () melaporkan hasil yang bukan file. Butuh waktu berjam-jam untuk menemukan sumber bug. Harap jangan gunakan tellg () untuk mendapatkan ukuran file. stackoverflow.com/questions/22984956/…
Puzomor Croatia
tidakkah sebaiknya Anda menelepon ifs.seekg(0, ios::end)sebelumnya tellg? tepat setelah membuka pointer membaca file di awal dan tellgmengembalikan nol
Andriy Tylychko
1
Anda juga perlu memeriksa file kosong karena Anda akan melakukan dereferensi nullptroleh&bytes[0]
Andriy Tylychko
ok, saya ketinggalan ios::ate, jadi saya pikir versi dengan eksplisit pindah ke akhir akan lebih mudah dibaca
Andriy Tylychko
50

Varian terpendek: Live On Coliru

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

Itu membutuhkan header <iterator>.

Ada beberapa laporan bahwa metode ini lebih lambat daripada mengalokasikan string dan menggunakan std::istream::read. Namun, pada kompiler modern dengan optimisasi yang diaktifkan ini tampaknya tidak lagi menjadi masalah, meskipun kinerja relatif dari berbagai metode tampaknya sangat bergantung pada kompiler.

Konrad Rudolph
sumber
7
Bisakah Anda melihat jawaban ini. Seberapa efisien itu, apakah itu membaca file char pada suatu waktu, lagi pula untuk mengalokasikan memori pengadukan?
Martin Beckett
@ MM Cara saya membaca perbandingan itu, metode ini lebih lambat daripada metode murni C ++ membaca-ke-a-preallocated-buffer.
Konrad Rudolph
Anda benar, ini adalah kasus judul berada di bawah sampel kode, daripada di atasnya :)
MM
@juzzlin C ++ tidak berfungsi seperti itu. Tidak memerlukan tajuk di lingkungan tertentu bukan alasan yang baik bagi Anda untuk tidak memasukkannya.
LF
Apakah metode ini memicu realokasi memori berkali-kali?
koin cheung
22

Menggunakan

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

atau sesuatu yang sangat dekat. Saya tidak memiliki referensi stdlib terbuka untuk memeriksa ulang sendiri.

Ya, saya mengerti saya tidak menulis slurpfungsi seperti yang diminta.

Ben Collins
sumber
Ini terlihat bagus, tetapi tidak dikompilasi. Perubahan untuk dikompilasi menguranginya menjadi jawaban lain di halaman ini. ideone.com/EyhfWm
JDiMatteo
5
Mengapa loop sementara?
Zitrax
Sepakat. Ketika operator>>dibaca menjadi std::basic_streambuf, ia akan mengkonsumsi (apa yang tersisa) aliran input, sehingga loop tidak perlu.
Remy Lebeau
15

Jika Anda memiliki C ++ 17 (std :: filesystem), ada juga cara ini (yang mendapatkan ukuran file melalui std::filesystem::file_sizealih-alih seekgdan 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;
}

Catatan : Anda mungkin perlu menggunakan <experimental/filesystem>dan std::experimental::filesystemjika perpustakaan standar Anda belum sepenuhnya mendukung C ++ 17. Anda mungkin juga perlu mengganti result.data()dengan &result[0]jika tidak mendukung data st-:: basic_string non-const .

Gabriel Majeri
sumber
1
Ini dapat menyebabkan perilaku yang tidak terdefinisi; membuka file dalam mode teks menghasilkan aliran yang berbeda dari file disk pada beberapa sistem operasi.
MM
1
Awalnya dikembangkan sebagai boost::filesystemAnda juga dapat menggunakan boost jika Anda tidak memiliki c ++ 17
Gerhard Burger
2
Membuka file dengan satu API dan mendapatkan ukurannya dengan yang lain tampaknya meminta kondisi inkonsistensi dan ras.
Arthur Tacca
14

Saya tidak memiliki reputasi yang cukup untuk mengomentari secara langsung tentang tanggapan menggunakan tellg().

Perlu diketahui bahwa tellg()dapat mengembalikan -1 pada kesalahan. Jika Anda melewatkan hasil tellg()sebagai parameter alokasi, Anda harus memeriksa hasilnya terlebih dahulu.

Contoh masalah:

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

Dalam contoh di atas, jika tellg()menemukan kesalahan, ia akan mengembalikan -1. Casting implisit antara ditandatangani (yaitu hasil tellg()) dan unsigned (yaitu arg ke vector<char>konstruktor) akan menghasilkan vektor Anda keliru mengalokasikan sangat sejumlah besar byte. (Mungkin 4294967295 byte, atau 4GB.)

Memodifikasi jawaban paxos1977 untuk akun di atas:

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

Solusi ini menambahkan pengecekan kesalahan ke metode berbasis 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();
}

Saya menambahkan jawaban ini karena menambahkan pengecekan kesalahan pada metode asli tidak sepele seperti yang Anda harapkan. Metode asli menggunakan operator penyisipan stringstream ( str_stream << file_stream.rdbuf()). Masalahnya adalah ini menetapkan failbit stringstream ketika tidak ada karakter yang dimasukkan. Itu bisa karena kesalahan atau bisa juga karena file sedang kosong. Jika Anda memeriksa kegagalan dengan memeriksa failbit, Anda akan menemukan false positive ketika Anda membaca file kosong. Bagaimana Anda membedakan kegagalan yang sah untuk menyisipkan karakter apa pun dan "kegagalan" untuk menyisipkan karakter apa pun karena file tersebut kosong?

Anda mungkin berpikir untuk secara eksplisit memeriksa file kosong, tapi itu lebih banyak kode dan pengecekan kesalahan yang terkait.

Memeriksa kondisi kegagalan str_stream.fail() && !str_stream.eof()tidak berfungsi, karena operasi penyisipan tidak mengatur eofbit (pada ostringstream atau ifstream).

Jadi, solusinya adalah mengubah operasi. Alih-alih menggunakan operator penyisipan ostringstream (<<), gunakan operator ekstraksi ifstream (>>), yang memang mengatur eofbit. Kemudian periksa kondisi kegagalan file_stream.fail() && !file_stream.eof().

Yang penting, ketika file_stream >> str_stream.rdbuf()menemukan kegagalan yang sah, seharusnya tidak pernah menetapkan eofbit (sesuai dengan pemahaman saya tentang spesifikasi). Itu berarti cek di atas cukup untuk mendeteksi kegagalan yang sah.

tgnottingham
sumber
3

Sesuatu seperti ini seharusnya tidak terlalu buruk:

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

Keuntungannya di sini adalah kita melakukan cadangan terlebih dahulu sehingga kita tidak perlu menumbuhkan talinya saat kita membaca banyak hal. Kerugiannya adalah kita melakukannya dengan char. Versi yang lebih cerdas dapat mengambil seluruh baca buf dan kemudian memanggil underflow.

Matt Price
sumber
1
Anda harus memeriksa versi kode ini yang menggunakan std :: vector untuk pembacaan awal dan bukan string. Jauh lebih cepat.
paxos1977
3

Ini adalah versi yang menggunakan pustaka sistem file baru dengan pemeriksaan kesalahan yang cukup kuat:

#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
sumber
infile.openjuga dapat menerima std::stringtanpa mengonversi dengan.c_str()
Matt Eding
filepathbukan std::string, itu a std::filesystem::path. Ternyata std::ifstream::openbisa menerima salah satu dari itu juga.
David G
@ David G, std::filesystem::pathsecara implisit dapat dikonversi kestd::string
Jeffrey Cash
Menurut cppreference.com, ::openfungsi anggota pada std::ifstreamyang menerima std::filesystem::pathberoperasi seolah-olah ::c_str()metode dipanggil di jalan. Yang mendasari ::value_typepath berada di charbawah POSIX.
David G
2

Anda dapat menggunakan fungsi 'std :: getline', dan tentukan 'eof' sebagai pembatas. Kode yang dihasilkan agak tidak jelas:

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
sumber
5
Saya baru saja menguji ini, tampaknya jauh lebih lambat daripada mendapatkan ukuran file dan memanggil read untuk seluruh ukuran file ke dalam buffer. Pada urutan 12x lebih lambat.
David
Ini hanya akan berfungsi, selama tidak ada karakter "eof" (mis. 0x00, 0xff, ...) dalam file Anda. Jika ada, Anda hanya akan membaca sebagian file.
Olaf Dietsche
2

Jangan pernah menulis ke buffer char * std :: string's str. Tidak akan pernah! Melakukan hal itu adalah kesalahan besar.

Cadangan () ruang untuk seluruh string di std :: string Anda, baca potongan dari file Anda dengan ukuran yang wajar ke dalam buffer, dan tambahkan () itu. Seberapa besar potongan harus tergantung pada ukuran file input Anda. Saya cukup yakin semua mekanisme portabel dan STL lainnya akan melakukan hal yang sama (namun mungkin terlihat lebih cantik).

Thorsten79
sumber
5
Sejak C ++ 11 dijamin OK untuk menulis langsung ke std::stringbuffer; dan saya percaya bahwa itu berfungsi dengan benar pada semua implementasi aktual sebelum itu
MM
1
Sejak C ++ 17 kita bahkan memiliki std::string::data()metode non-const untuk memodifikasi buffer string secara langsung tanpa menggunakan trik seperti &str[0].
zett42
Setuju dengan @ zett42 jawaban ini sebenarnya tidak benar
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);
}

pemakaian:

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

Fungsi yang diperbarui yang didasarkan pada solusi 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;
}

Ada dua perbedaan penting:

tellg()tidak dijamin untuk mengembalikan offset dalam byte sejak awal file. Sebagai gantinya, seperti yang ditunjukkan Puzomor Croatia, itu lebih merupakan token yang dapat digunakan dalam panggilan fstream. gcount()Namun tidak mengembalikan jumlah byte terformat lalu diekstrak. Karena itu kami membuka file, mengekstrak dan membuang semua isinya dengan ignore()untuk mendapatkan ukuran file, dan membangun string keluaran berdasarkan itu.

Kedua, kami menghindari harus menyalin data file dari a std::vector<char>ke a std::stringdengan menulis ke string secara langsung.

Dalam hal kinerja, ini harus menjadi yang tercepat mutlak, mengalokasikan string berukuran yang sesuai sebelumnya dan menelepon read()sekali. Sebagai fakta yang menarik, menggunakan ignore()dan countg()bukannya atedan tellg()pada gcc mengkompilasi ke hal yang hampir sama , sedikit demi sedikit.

Kiroma
sumber
1
Kode ini tidak berfungsi, saya mendapatkan string kosong. Saya pikir Anda ingin ifs.seekg(0)alih-alih ifs.clear()(kemudian berfungsi).
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
sumber
1
Silakan tambahkan deskripsi.
Peter
silakan kunjungi dan periksa bagaimana menjawab pertanyaan .
Yunus Temurlenk