What is the easiest way to read and write data from a structure to and from a file in C ++ without a serialization library?

I am writing a program that regularly stores and reads structures in the form below.

struct Node { int leftChild = 0; int rightChild = 0; std::string value; int count = 1; int balanceFactor = 0; }; 

How will I read and write nodes to a file? I would like to use the fstream class with seekg and seekp to do the serialization manually, but I'm not sure how this works based on the documentation and I'm afraid to find decent examples.

[edit] indicates that I do not want to use the serialization library.

+5
source share
6 answers

To serialize objects, you need to adhere to the concept that an object writes its members to a stream and reads elements from the stream. In addition, member objects must be written to the stream (as well as read).

I implemented a scheme using three member functions and a buffer:

 void load_from_buffer(uint8_t * & buffer_pointer); void store_to_buffer(uint8_t * & buffer_pointer) const; unsigned int size_on_stream() const; 

size_on_stream will be called first to determine the size of the buffer for the object (or how much space it takes in the buffer).

The load_from_buffer function loads the elements of an object from the buffer using the specified pointer. The function also increments the pointer accordingly.

The store_to_buffer function stores the elements of objects in the buffer using the specified pointer. The function also increments the pointer accordingly.

This can be applied to POD types using templates and specialized templates.

These features also allow you to pack output into a buffer and load from a packed format.

The reason for the I / O for the buffer is to use more efficient block-stream methods, such as write and read .

Edit 1: Writing node to stream
The problem with writing or serializing a node (such a linked list or node tree) is that pointers are not translated to the file. There is no guarantee that the OS will place your program in the same memory location or give you the same memory area each time.

You have two options: 1) Save data only. 2) Convert pointers to file offsets. Option 2) is very complicated, as it may require re-setting the file pointer, since file offsets may not be known in advance.

Also note variable length entries such as strings. You cannot directly write a string object to a file. If you do not use a fixed line width, the line size will change. You will need to either prefix a string with a string length (preferred) or use some kind of trailing character, such as '\ 0'. The length of the string is preferable at first because you do not need to look for the end of the string; you can use a block read for reading in text.

+3
source

This problem is called serialization . Use a serializing library, for example, for example. Google Protocol Buffers or Flatbuffers .

+4
source

If you replace std :: string with a char buffer, you can use fwrite and fread to write / read your structure to and from disk as a fixed-size information block. As part of a program that should work fine.

The big bug-a-boo is the fact that compilers will insert indents between fields to maintain data alignment. This makes the code less portable, as if the module was compiled with different alignment requirements, the structure can literally be of a different size, choosing the assumption of a fixed size from the door.

I would lean toward a well-worn serialization library.

+1
source

Another approach would be to overload the <operator and the β†’ operator for the structure so that it knows how to save / load itself. This will reduce the problem by knowing where to read / write the node. Theoretically, your left and right child fields can look up addresses where the nodes are actually located, while a new field can hold the search location of the current node.

+1
source

When introducing your own serialization method, the first decision you will need to make is whether you want the data on the disk to be in binary or text format.

It’s easier for me to implement the ability to save in binary format. The number of functions required for its implementation is small. You need to implement functions that can write basic types, arrays of known sizes at compile time, dynamic arrays and strings. Everything else can be built on top of them.

Here is something very close to what I recently introduced into production code.

 #include <cstring> #include <fstream> #include <cstddef> #include <stdexcept> // Class to write to a stream struct Writer { std::ostream& out_; Writer(std::ostream& out) : out_(out) {} // Write the fundamental types template <typename T> void write(T number) { out_.write(reinterpret_cast<char const*>(&number), sizeof(number)); if (!out_ ) { throw std::runtime_error("Unable to write a number"); } } // Write arrays whose size is known at compile time template <typename T, uint64_t N> void write(T (&array)[N]) { for(uint64_t i = 0; i < N; ++i ) { write(array[i]); } } // Write dynamic arrays template <typename T> void write(T array[], uint64_t size) { write(size); for(uint64_t i = 0; i < size; ++i ) { write(array[i]); } } // Write strings void write(std::string const& str) { write(str.c_str(), str.size()); } void write(char const* str) { write(str, std::strlen(str)); } }; 

 // Class to read from a stream struct Reader { std::ifstream& in_; Reader(std::ifstream& in) : in_(in) {} template <typename T> void read(T& number) { in_.read(reinterpret_cast<char*>(&number), sizeof(number)); if (!in_ ) { throw std::runtime_error("Unable to read a number."); } } template <typename T, uint64_t N> void read(T (&array)[N]) { for(uint64_t i = 0; i < N; ++i ) { read(array[i]); } } template <typename T> void read(T*& array) { uint64_t size; read(size); array = new T[size]; for(uint64_t i = 0; i < size; ++i ) { read(array[i]); } } void read(std::string& str) { char* s; read(s); str = s; delete [] s; } }; 

 // Test the code. #include <iostream> void writeData(std::string const& file) { std::ofstream out(file); Writer w(out); w.write(10); w.write(20.f); w.write(200.456); w.write("Test String"); } void readData(std::string const& file) { std::ifstream in(file); Reader r(in); int i; r.read(i); std::cout << "i: " << i << std::endl; float f; r.read(f); std::cout << "f: " << f << std::endl; double d; r.read(d); std::cout << "d: " << d << std::endl; std::string s; r.read(s); std::cout << "s: " << s << std::endl; } void testWriteAndRead(std::string const& file) { writeData(file); readData(file); } int main() { testWriteAndRead("test.bin"); return 0; } 

Output:

 i: 10 f: 20 d: 200.456 s: Test String 

The ability to write and read Node very easy to implement.

 void write(Writer& w, Node const& n) { w.write(n.leftChild); w.write(n.rightChild); w.write(n.value); w.write(n.count); w.write(n.balanceFactor); } void read(Reader& r, Node& n) { r.read(n.leftChild); r.read(n.rightChild); r.read(n.value); r.read(n.count); r.read(n.balanceFactor); } 
+1
source

The process you are talking about is known as serialization. I would recommend Cereal at http://uscilab.imtqy.com/cereal/

It supports both json, xml, and binary serialization and is very easy to use (with good examples).

(Unfortunately, it does not support my favorite yaml format)

0
source

Source: https://habr.com/ru/post/1246958/


All Articles