The following code is my answer to a Stack Overflow question:
#include <string>
#include <vector>
#include <fstream>
#include <cstdio>
#include <cassert>
// #include <stdint.h> => DWORD
#ifndef int32_t
#define uint32_t DWORD
#define int32_t int
#endif
struct MapItem
{
std::string term;
std::vector<int32_t> pl;
// this function also emphasizes the way this objects are serialized to file; if something changes here,
// ReadNext & WriteNext must also be updated (unfortunate dependency)
size_t SizeWrittenToFile() const
{
return 2*sizeof(uint32_t)+term.length()+pl.size()*sizeof(int32_t);
}
};
bool ReadNext(std::istream& in, MapItem& item)
{
size_t size;
in.read(reinterpret_cast<char*>(&size), sizeof(uint32_t));
if (!in)
return false;
std::istreambuf_iterator<char> itIn(in);
std::string& out = item.term;
out.reserve(size);
out.clear(); // this is necessary if the string is not empty
// copy_n available in C++11
for (std::insert_iterator<std::string> itOut(out, out.begin()); in && (out.length() < size); itIn++, itOut++)
*itOut = *itIn;
assert(in);
if (!in)
return false;
in.read(reinterpret_cast<char*>(&size), sizeof(uint32_t));
if (!in)
return false;
std::vector<int32_t>& out2 = item.pl;
out2.resize(size); // reserve doesn't work here
in.read(reinterpret_cast<char*>(&out2[0]), size * sizeof(int32_t));
assert(in);
return true;
}
// a "header" should be added to mark complete data (to write "atomically")
bool WriteNext(std::ostream& out, const MapItem& item)
{
size_t size = item.term.length();
out.write(reinterpret_cast<const char*>(&size), sizeof(uint32_t));
if (!out)
return false;
out.write(item.term.c_str(), size);
if (!out)
return false;
size = item.pl.size();
out.write(reinterpret_cast<const char*>(&size), sizeof(uint32_t));
if (!out)
return false;
out.write(reinterpret_cast<const char*>(&item.pl[0]), size * sizeof(int32_t));
if (!out)
return false;
return true;
}
bool UpdateItem(std::ifstream& in, std::ofstream& out, const MapItem& item)
{
MapItem it;
bool result;
for (result = ReadNext(in, it); result && (it.term != item.term); result = ReadNext(in, it))
if (!WriteNext(out, it))
return false;
if (!result)
return false;
assert(it.term == item.term);
if (!WriteNext(out, item))
return false;
for (result = ReadNext(in, it); result; result = ReadNext(in, it))
if (!WriteNext(out, it))
return false;
return in.eof();
}
bool UpdateItem(const char* filename, const MapItem& item)
{
std::ifstream in(filename, std::ios_base::in | std::ios_base::binary);
assert(in);
std::string filename2(filename);
filename2 += ".tmp";
std::ofstream out(filename2.c_str(), std::ios_base::out | std::ios_base::binary);
assert(out);
bool result = UpdateItem(in, out, item);
in.close();
out.close();
int err = 0;
if (result)
{
err = remove(filename);
assert(!err && "remov_140");
result = !err;
}
if (!result)
{
err = remove(filename2.c_str());
assert(!err && "remov_147");
}
else
{
err = rename(filename2.c_str(), filename);
assert(!err && "renam_151");
result = !err;
}
return result;
}
Beside correctness, I'm concerned with two problems:
- performance when reading to string in
ReadNext
: "in && (out.length() < size)
" A better implementation would be capable of reading directly a block of data. - To update an item a second file is used. Is there a safe solution using only one file?
EDIT My interess goes beyond the initial Stack Overflow question as this is a (re)learning exercise of the C++ streams and I'll use it as a sample. I also added the code here, with additional real tests, VS 2008, but it should be easily ported to other platforms.