Type tag for identifying types that can be read / written in binary form

Is there a type (or concept) of type for identifying those types for which the following is safe:

template <typename T> std::enable_if_t<std::some_type_trait<T>::value> Write(std::ostream &os,const T &x) { os.write(reinterpret_cast<const char *>(&x),sizeof(T)); } template <typename T> std::enable_if_t<std::some_type_trait<T>::value> Read(std::istream &is,T &x) { is.read(reinterpret_cast<char *>(&x),sizeof(T)); } 

Im thinking of classes containing POD, excluding pointers (but not arrays). Something like StandardLayoutType , but without pointers. I do not want to limit the TrivialType and TriviallyCopyable .

Sorry if Im inaccurate. I know little about data presentation.

+6
source share
2 answers

For parameter 1 st s the read method:

Extracts characters and stores them in consecutive locations of the character array, the first element of which is indicated by s

So your real question is: If I initialized the object by writing a string of bytes to it, is it really?

This is the concept of value . And the representation of the value of the Trivially Copyable type is such that:

Copying the bytes occupied by an object in storage is enough to create another object with the same value

Thus, you want your object to be Trivially Copyable, this does not mean a standard concept, but it can be briefly described as:

The spirit of the assertion that there is at least one trivial initializer for an object comes down to these requirements of the type of a trivially copied type, its non-static elements and any of its base classes:

  • It defines a trivial initializer or behaves as the corresponding standard initializer
  • It has no virtual methods
  • He has no members of the category with variable qualifications

Regarding the requirement of a trivial destructor:

  • The destructor is not provided to users (this means that it is either implicitly declared or explicitly defined as default by its first declaration)
  • The destructor is not virtual (i.e. the base class destructor is not virtual)
  • All direct base classes have trivial destructors.
  • All non-static data members of a class type (or an array of a class type) have trivial destructors

Having fully defined what it means to be a trivially copied type, it is impossible for a “type or concept of type” to determine whether all these requirements are satisfied in all cases, for example: a type that defines a trivial initializer with a signature that corresponds to the default initializer may or may not can be a trivially copied code contingent that initializes the type in this initializer element; For this type, the only way to determine if it is trivially copied is to check the initializer by the person. If, however, you want to tighten the requirements for what is detected, is_trivially_copyable ensures that your type is trivially copied.

+5
source

No no.

If we had a full reflection (approaching the C ++ standard next to you 2014 2017 2020!), You could write your own character to some extent.

But even there you ran into a problem.

A std::size_t may be a value or an index in some const char* hash table, sown at their memory location. It is unsafe to write out such a value, and then read further at the next program execution.

Also, the reading code may not agree with how large a int . So now you have to distinguish between int32_t and int , both of which are allowed for the same type in compiler 1, but different types in compiler 2 (or even in compiler settings!).

It is best to use your own trait like the Koenig flag function, which claims that something is safe for binary serialization, with some excessive security checks (generate errors if the type no longer matches, standard layout, trivially copied, etc.).

In addition, you must use the archive system. Add a reflection that allows you to read / write the state of the object. Make aggregation objects easy recursively.

 template<class Stream> void Archive( Stream& s ) { s.start(*this)->*[&]{ s & field1; s & field2; s & field3; }; } 

where this code breaks down into a reading or writing mechanism depending on the type of Stream , encodes a header and (optionally) a type flag for *this and length. Then it transfers the contents to / from. Additional materials at the end are automatically discarded.

For binary types:

 template<class Stream> void Archive( Stream& s ) { FlatBinary( *this, s ); } 

does everything for you, but still guarantees alignment of sizes, etc. (letting the structure grow in future versions without any errors!) We can even detect flat binary types and not use Archive through the flag.

Placed:

 friend std::true_type is_flat_binary_test( BobType ) { return {}; } 

in these types. Then do

 namespace flat_binary_details { template<class T> inline std::false_type is_flat_binary_test( T ) { return {}; } template<class T> inline auto flat_binary_f() -> decltype( is_flat_binary_test( std::declval<T>() ) ) { return {}; } } template<class T> using is_flat_binary = decltype( details::flag_binary_f<T>() ); 

Now is_flat_binary< std::vector<int> > is false_type , and

 namespace X { struct Bob { friend std::true_type is_flat_binary_test( Bob ); // body optional }; } static_assert( is_flat_binary<X::Bob>{}, "Bob is flat!" ); 

just works.

Your archive system can test all objects marked as flat binary files and implement an efficient archiving system for them. Without it, he can detect a member of the Archive( Stream& ) and call it. Without it, it can detect Read and Write . Custom design types can be extended. You can write a non-member Achive for std types.

But it is too far. In short, archiving material is difficult; find the structure.

+2
source

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


All Articles