1

I am trying to implement a generic serialization method, that I would like to use to serialize/ de-serialize generic custom structures with the following code:

#include <string>
#include <iostream>

class MessageSerializer
{
public: 
    MessageSerializer(){}

    ~MessageSerializer(){}

    template <typename Data>
    std::string serialize(const Data data)
    {
        const char* lpData = reinterpret_cast<const char*>(&data);
        std::string serializedData( lpData, sizeof(data));
        return serializedData;
    } 

    template <typename Data> 
    void deSerialize( const std::string &serializedData, Data &deserializedObject )
    {
        const size_t serializedDataSize = serializedData.size();
        const size_t outputDataSize = sizeof(Data);

        if ( serializedDataSize != outputDataSize ){
            std::cout << "Failed size check" << std::endl; 
        }

        std::copy(std::begin(serializedData), std::end(serializedData), reinterpret_cast<char*>(&deserializedObject));
    }
};

#pragma pack(push, 1)
struct testStruct 
{
    testStruct(int a, double f, std::string text, char c) : _a(a), _f(f), _text(text), _c(c){}
    testStruct(){}

    int _a; 
    double _f; 
    std::string _text;
    char _c;
};
#pragma pack(pop) ```

When using it in my main:



    #include "serializer.hpp"
#include <iostream>
#include <string>

int main ()
{
    MessageSerializer ser;
    testStruct sender(1, 345.234, "this is a test string", 'h'); 

    testStruct receiver; 
    std::string msg = ser.serialize(sender);
    std::cout <<"serialized: " << msg << std::endl; 

    std::cout << "-------------" << std::endl;

    ser.deSerialize(msg, receiver);
    
    std::cout << "received content: \na= " << receiver._a 
                << "\nf: " << receiver._f 
                << "\ntext: " << receiver._text 
                << "\nc: " << receiver._c << std::endl;
    return 0; 
}

It seems to work, after my main ends I end up receiving the following error message:

free(): double free detected in tcache 2 Aborted (core dumped)

I seem unable to find the exact cause of that. After some research I understand that apparently a resource is being freed twice that has not been allocated a second time, but I am not allocating any memory on the heap anywhere at all.

Can anybody help me understand the cause of my problem, please?

RoBo
  • 43
  • 7
  • 1
    Two quick observations: (1) You cannot use `sizeof` with a structure containing `string` or other complex objects (non plain). (2) The size of data depends on architecture. Therefore the saved file may not be read, using the same code, on a different machine. – zdf Jan 28 '21 at 14:24
  • Isn't #pragma pack(push, 1) taking care of exactly that, since it will ensure there is no padding, making it architecture independent? – RoBo Jan 28 '21 at 15:07
  • @RoBo removing padding doesn't undo the fact that the size of (for example) an `int` is not the same across architectures... @zdf – gthanop Jan 28 '21 at 15:27
  • Right you are. I forgot about that. Thank you! – RoBo Jan 28 '21 at 16:59

1 Answers1

1

One problem I can think of, is the fact that sizeof(testStruct) is actually almost equal to the size of each element type it contains, ie:

sizeof(int) + sizeof(double) + sizeof(std::string) + sizeof(char)

The resulting size does not contain the whole actual contents of the std::string object (which are "this is a test string"), but instead it results to something more like the size of the primitive data types that testStruct contains plus the size of the primitive data types that std:string contains (which should be a pointer to the contents and not the contents themselves).

So, because you pass your testStruct by value into the serialize method, it should be like copying the pointer to the "this is a test string" character sequence which is going to get freed upon destructing the contained std::string object upon the serialize method exit, while the same pointer is going to be freed also when the program finishes (because it is also declared in the main as the sender's std::string object).

You just wrapped your std::string into the testStruct struct (which has a destructor?), but this could probably occur if your just tested it with only a plain std::string object as your sender value.

It seems that this is what is happening, but I am not entirely sure, so correct me if I'm wrong in any assumption/hypothesis.

Edit 1:

As an answer to your first comment: you cannot use a char* for the text because this would mean that again the contents of the text would not be copied. Only the value of the actual pointer would be encoded. You could probably use a char array of fixed size (for example say char[100] or something) and store your text there, but that:

  1. Introduces the problem of having to know how much exactly is the maximum of characters you are going to store, and also that you have to allocate all of them even if not as many are needed for an instance of your testStruct.
  2. Does not change the fact that your binary representation is not going to be portable, because of the size of each primitive data type being different accross architectures.

I would suggest you to have a look at the following link:
https://isocpp.org/wiki/faq/serialization
and read it a little. It might help.

If you want text representation (such as XML, or even plain custom-structured text) remember that it should again be non-portable because for example if you print a number of type int with the value (for example) 2^30 then in another architecture where the int is of 2 bytes size, the value would overflow. But maybe you can get around this with cstdint types (such as int_least32_t and so on), but then you have to encode all your data into numbers, which might not seem too hard, and it probably isn't, but that includes text which would mean using a specific text encoding table, ie ASCII, UTF-8, etc...

You can also have a look at another post about serializing an object in C++.

If you want binary representation and portability and easy implementation, I would suggest to simply change your language to Java if at all possible and then have a look at the Serializable interface and its rules. The difference with Java is that it has primitive data types of fixed size and that does not depend on the architecture, as far as I know.

If you only want to copy complex objects accross the same process, use copy constructors.

gthanop
  • 3,035
  • 2
  • 10
  • 27
  • Thank you for the explanation. This sure makes sense to me. Would you have a suggestion as to how I might be able to solve that issue? Perhaps use a char array instead of a string, since then I would be able to determine the exact size – RoBo Jan 28 '21 at 15:29
  • Hi @RoBo. No problem. I hope I am saying this correctly and I hope some more experienced users will correct me if I am wrong. Check my **Edit 1** section in the answer, which has some thoughts on solving your problem. – gthanop Jan 28 '21 at 16:31
  • 1
    Thanks for the clear and most helpful explanation. I will have a look into the documentation you kindly provided and will move from there – RoBo Jan 28 '21 at 17:01