Why does fstream not working correctly when I'm trying to use read and write?

Viewed 125

I'm trying to create a local file database using fstream and map. I'm using class fstream to read and write. But it is creating an empty file, I tried with operator >>, << and with function read(), write(). And it also not working.

Here is code:

#include <iostream>
#include <map>
#include <fstream>
#include <string>
using str = std::string;
class MapDB{
    public:
        MapDB(str path){
            this->dbfile.open(path, std::fstream::in | std::fstream::out | std::fstream::trunc);
        }
        void PushValue(str key, str value){
            this->Load();
            this->db[key] = value;
            this->Save();
        }
        str GetValue(str key){
            this->Load();
            return this->db[key];
        }
        void RemoveKey(str key){
            this->db.erase(key);
            this->Save();
        }
        ~MapDB(){
            this->dbfile.close();
        }
    private:
        void Load(){
            //this->dbfile.read((char*)&this->db, sizeof(MapDB));
            this->dbfile >> (char*)&this->db;
        }
        void Save(){
            this->dbfile.clear();
            //this->dbfile.write((char*)&this->db, sizeof(MapDB));
            this->dbfile << (char*)&this->db;
        }
        std::fstream dbfile;
        std::map<str, str> db;
};
int main(int argc, char *argv[]){
    std::map<str, str> mydb;
    MapDB mydata("/path/to/file/data.data");
    mydata.PushValue("key", "value");
    std::cout << mydata.GetValue("key") << std::endl;
}

any ideas?

3 Answers

Multiple problems exist in your code:

  1. std::map<> is not trivial and cannot be serialized the way you try to do it. You have to serialize it yourself - item by item.
  2. dbfile.clear(); only clears the error flags of the stream which is probably not what you want to do.
  3. Load() should position the read cursor, Save() should position the write cursor.
  4. Your constructor truncates the file. So there is no chance to read something that has been written by another instance of MapDB. (Maybe this is indentionally)

I would not risk to say this list is nearly complete. Hopefully this example will give you a few hints:

class MyDB
{
public:
    // constructor.
    // creates or opens the file in binary mode (because we store binary data like the number of items and, the length of the strings).
    // truncates the file on open if the flag 'truncateOnOpen' is set.
    MyDB(const std::string& filename, bool truncateOnOpen)
        : dbfile(filename, std::fstream::binary | std::fstream::in | std::fstream::out | (truncateOnOpen ? std::fstream::trunc : 0))
    {}

    void Load()
    {
        // drop old database content
        db.clear();

        // position read cursor to the beginning of the file
        dbfile.seekg(0);

        // read the number of entries
        size_t entries = 0;
        dbfile.read(reinterpret_cast<char*>(&entries), sizeof(entries));

        // read key and value for each entry
        std::string key, value;
        for (size_t i = 0; i < entries; ++i)
        {
            readString(&key);
            readString(&value);
            db[key] = value;
        }
    }

    void Save()
    {
        // position the write cursor to the beginning of the file
        dbfile.seekp(0);

        // write thenumber of entries
        size_t entries = db.size();
        dbfile.write(reinterpret_cast<const char*>(&entries), sizeof(entries));

        // write key and value for each entry
        for (auto& it : db)
        {
            writeString(it.first);
            writeString(it.second);
        }
    }

private:
    // reads a single string from the file
    bool readString(std::string* target)
    {
        // read the length of the string stored in the file
        size_t len;
        if (dbfile.read(reinterpret_cast<char*>(&len), sizeof(len)).fail())
            return false;

        // preallocate memory for the string
        target->resize(len);

        // read the string from the file
        return dbfile.read(&target->front(), target->size()).good();
    }

    // writes a single string to the file
    void writeString(const std::string& source)
    {
        // write the length of the string to the file
        size_t len = source.size();
        dbfile.write(reinterpret_cast<char*>(&len), sizeof(len));
        
        // write the string itself to the file
        dbfile.write(source.data(), source.size());
    }

private:
    std::fstream dbfile;

public:
    std::map<std::string, std::string> db;
};

Ok, lets go through it step by step:

But it is creating an empty file

That is because you are emptying the file with std::fstream::trunc - Delete everything in the file (truncate it) Meaning the moment you open your data.data file you erase all data in it if any is already present.


There are two standard formats in 'std::fstream':

  1. format is the std::ios::binary format, data.data is not readable with a text editor but it is compact and efficient. Y

  2. format is storing the strings in printable characters. in that case data.data is readable like a txt-file with a common file editor.

Since @Andreas H. provided a binary file format example and you didn't used the binary format, I provide a minimal executable example that stores your data with printable characters.

  1. int main(int argc, char *argv[]) needs [ ]-brackets to work properly

  2. Type casting in the old c-style (char*)&this->db; is working fine, Scott Meyers writes in "Effective C++" old c-style should be avoided because because old-c type casting is not type safe and "hides" in the code so it is not easy to find like static_cast<char*>(...) Here you see the difference - both are doing the same thing:

int firstNumber = 1, secondNumber = 2;

double result1 = ((double)firstNumber) /secondNumber;
double result2 = static_cast<double>(firstNumber)/secondNumber;

std::cout << result1 << std::endl;
std::cout << result2 << std::endl;

For the sake of providing a minimal executable example and a better understanding I skipped the class ;) Should be easy enough for you to parse it in a nice interface.


#include <iostream>
#include <map>
#include <fstream>
#include <string>

int main(int argc, char *argv[])
{

  std::map<std::string, std::string> db{{"Key1","value"},{"Key2","value"}};
  std::fstream mydata;

  //PRINT MAP
  for(const auto &[k,v] : db)
    std::cout << k << " " << v << std::endl;

  //WRITE MAP TO FILE
  //Open File in write mode
  mydata.open("data.data", std::ios::out);
  if(!mydata.is_open())
  std::cout << "Could not open file!" << std::endl;

  //Write data to file
  for(const auto &[k,v] : db)
    mydata << k <<" " << v << std::endl;
  //Close file
  mydata.close();

  //proof of concept, DESTROY MAP VALUES
  db.clear();

  //READ MAP BACK IN FROM FILE
  std::string key , value;

  //Open Data in read mode
  mydata.open("data.data", std::ios::in);
  if(!mydata.is_open())
    std::cout << "Could not open file!" << std::endl;

  //Read data back to map
  while(mydata >> key >> value)
    db[key] = value;

  //Close File again
  mydata.close();

  //PRINT MAP
  for(const auto &[k,v] : db){
    std::cout << k << " " << v << std::endl;
  }

  return 0;
}

I hope this will help you finish your project :)

Btw. if you have more than one word as key or value, you can use getline with a delimiter, and store your data file in a csv-format (coma separated values file) for easy import of your data f.e. in email software if you store contact data in your database.

Just as an exercise for me I wrote a minimal executable example to write a map of strings in a binary format to a file:

#include <iostream>
#include <fstream>
#include <map>
#include <string>

#define FILENAME "BinaryMap.bin"

int main()
{
  std::fstream file;
  std::string key{"Key One"}, value{"Value One"};
  std::map<std::string, std::string> map{{key,value},{"Key Two","Value Two"}};


  //Print Map
  for(const auto &[key,value] : map){
    std::cout << key << " " << value << '\n';
  }

  //Open file and check for errors
  file.open(FILENAME, std::ios::binary | std::ios::out);
  if(!file.is_open())
  std::cout << "Could not open file!" << '\n';

  //Position write cursor to the beginning of the file
  file.seekp(0);

  //Write the number of entries
  size_t entries = map.size();
  file.write(reinterpret_cast<const char*>(&entries), sizeof(entries));

  //Write the number of entries
  for(auto& it : map){
    //Write the lenth of the key string to the file
    size_t len = it.first.size();
    file.write(reinterpret_cast<char*>(&len), sizeof(len));

    //Write the string itself to the file
    file.write(it.first.data(), it.first.size());

    //Write the length of the value string to the file
    len = it.second.size();
    file.write(reinterpret_cast<char*>(&len), sizeof(len));

    //Write the value string itself to the file
    file.write(it.second.data(), it.second.size());
  }
  //Close the file
  file.close();

  map.clear();
  std::cout << "--------clear---------" << '\n';

  //READ STUFF
  file.open(FILENAME, std::ios::binary | std::ios::in);
  if(!file.is_open())
    std::cout << "Could not open file!" << '\n';

  // Position read cursor to the beginning
  file.seekg(0);

  // read the number of entries
  entries = 0;
  file.read(reinterpret_cast<char*>(&entries), sizeof(entries));

  //Read the length of the string stored in the file
  size_t length {};
  for(size_t i = 0; i < entries; ++i){
    //Read the length of the string in the file
    if(file.read(reinterpret_cast<char*>(&length), sizeof(length)).fail())
        std::cout << "Failed to read bin" << '\n';

    //Pointer to variable address
    std::string* p_target = &key;

    //Preallocate memory for the string
    p_target->resize(length);

    //Read string from file
    file.read(&p_target->front(), p_target->size()).good();

    //Read the length of the string in the file
    if(file.read(reinterpret_cast<char*>(&length), sizeof(length)).fail())
      std::cout << "Failed to read bin" << '\n';

    //Dereference value
    key = *p_target;

    //Assign variable address to pointer
    p_target = &value;

    //Preallocate memory for the string
    p_target->resize(length);

    //Read string from file
    file.read(&p_target->front(), p_target->size()).good();

    //Dereference value
    value = *p_target;

    //Write key and value to map
    map[key] = value;
  }

  //Close the file
  file.close();

  for(const auto &[key,value] : map){
    std::cout << key << " " << value << '\n';
  }

  std::cout << "Hit 'return' to continue";
  std::cin.get();
  return 0;
}

It's not fstream that isn't working correctly. fstream works fine.

For this to work:

        this->dbfile >> (char*)&this->db;

and this:

        this->dbfile << (char*)&this->db;

... I'm not even sure what it would take. Imagine a possible implementation of:

std::map<str, str> db;

It's not going to just be a raw stream of data with a null byte on the end. It's going to be quite complicated, and it's going to have pointers. Even if you could somehow tell the << to write the number of characters you want, you'd be writing out pointers that point somewhere totally else, so reading them in later won't do you one bit of good.

Instead, you need to somehow to do it by hand, probably by iterating over the contents of the map and printing them out, maybe like this:

dbFile << dbElement.first << " == " << dbElement.second << endl;

Of course, that's not going to be safe if the data contains newlines. But at least you'd get closer. Then you could read lines in with getline and parse the line, splitting it into two pieces based on " == " as a divider.

It's probably 20 or 30 lines of code in all.

Related