Динамический массив структур. Запись и чтение из файла

Есть такая структура:

    struct Rent
{
    char* obj;
    char* date;
    unsigned int beg;
    unsigned int end;
    char* name;
};

Есть массив структур:

Rent* arr = new Rent[n];

И массив указателей на строковые константы:

const char* mas[8] = { "Велосипед", "400", "Самокат", "350", "Ролики", "300", "Скейтборд", "200" };

Запись в файл:

ofstream fout;
    fout.open("rent.txt", ios::binary | ios::trunc);
    fout.write(reinterpret_cast<char*>(mas), 8 * sizeof(const char*));
    fout.write(reinterpret_cast<char*>(arr), n * sizeof(Rent));
    fout.close();

Чтение из файла:

int n;
    Rent* arr2;
    const char* mas2[8];
    ifstream fin;
    std::ifstream num("rent.txt", std::ios::binary);

    num.seekg(0, std::ios::end);
    n = ((long)num.tellg()-8) / sizeof(Rent);
    n--;
    num.seekg(0, std::ios::beg);
    num.close();;
    arr2 = new Rent[n]; 

    fin.open("rent.txt", std::ios::binary);
    fin.read(reinterpret_cast<char*>(mas2), 8 * sizeof(const char*));
    fin.read(reinterpret_cast<char*>(arr2), n * sizeof(Rent));
    fin.close();

Смысл был такой: из консоли считываем массив структур, записываем файл, из этого файла считываем в другой массив. При чтении из консоли динамическая память выделяется, там все нормально. И для вторых массивов я память не выделяла (именно для char*), и все выводилась нормально. Я правильно понимаю, что из-за того, что в файле хранятся адреса строк? Мне нужно, чтобы в файле хранились именно строки, и их можно было считать в любой момент. Как это сделать?


Ответы (4 шт):

Автор решения: Никита Самоуков

Нужно сделать для каждого класса 3 функции:

  1. уникальный ид типа
  2. сериализация
  3. десериализация

Соответственно в файл писать блоки вида: <ид_типа><данные_типа>

Делается такое примерно так:

#include <iostream>
#include <string>
#include <array>
#include <utility>
#include <vector>
#include <algorithm>
#include <cstdint>
#include <chrono>
#include <random>
#include <numeric>
#include <unordered_map>
#include <typeindex>
#include <functional>
#include <any>
#include <span>
#include <unordered_set>
#include <optional>
#include <cassert>

using namespace std;

// Basics


template<typename T>
void SerializeRawSimple(T const& val, vector<byte>& data)
{
    union U
    {
        T val;
        array<byte, sizeof T> b;
    };

    U u{ .val = val };
    data.insert(data.end(), u.b.begin(), u.b.end());
}

void SerializeRaw(int32_t const& val, vector<byte>& data)
{
    SerializeRawSimple(val, data);
}

void SerializeRaw(int64_t const& val, vector<byte>& data)
{
    SerializeRawSimple(val, data);
}

void SerializeRaw(string const& val, vector<byte>& data)
{
    SerializeRawSimple((int64_t)val.size(), data);
    for (auto& c : val)
    {
        data.push_back((byte)c);
    }
}


template<typename T>
T DeserializeRawSimple(span<const byte>const& data, int64_t& type_data_size)
{
    union U
    {
        array<byte, sizeof T> b;
        T val;
    };

    if (data.size() < sizeof T)
        throw "Deserialize error";

    U u;
    copy(data.begin(), data.begin() + sizeof T, u.b.begin());
    type_data_size = sizeof T;
    return u.val;
}

template<typename T>
T DeserializeRaw(span<const byte>const& data, int64_t& type_data_size)
{
    static_assert(false && "Need implement");
    return T();
}

template<>
int32_t DeserializeRaw<int32_t>(span<const byte> const& data, int64_t& type_data_size)
{
    return DeserializeRawSimple<int32_t>(data, type_data_size);
}

template<>
int64_t DeserializeRaw<int64_t>(span<const byte>const& data, int64_t& type_data_size)
{
    return DeserializeRawSimple<int64_t>(data, type_data_size);
}

template<>
string DeserializeRaw<string>(std::span<const byte>const& data, int64_t& type_data_size)
{
    auto data_span = data;
    int64_t tmp_size;
    type_data_size = 0;
    string res;

    auto str_len = DeserializeRaw<int64_t>(data_span, tmp_size);
    data_span = data_span.subspan(tmp_size);
    type_data_size += tmp_size;

    if (data_span.size() < str_len)
        throw "Deserialize error";
    res.resize(str_len);
    for (size_t i = 0; i < str_len; i++)
        res[i] = (char)data_span[i];
    type_data_size += str_len;

    return res;
}




// Types

struct A
{
    int32_t x = 8;
    int32_t y = 9;
    string z = "12345";
};

void SerializeRaw(A const& val, vector<byte>& data)
{
    SerializeRaw(val.x, data);
    SerializeRaw(val.y, data);
    SerializeRaw(val.z, data);
}

template<>
A DeserializeRaw<A>(std::span<const byte>const& data, int64_t& type_data_size)
{
    auto data_span = data;
    int64_t tmp_size;
    type_data_size = 0;
    A res;

    res.x = DeserializeRaw<int32_t>(data_span, tmp_size);
    data_span = data_span.subspan(tmp_size);
    type_data_size += tmp_size;

    res.y = DeserializeRaw<int32_t>(data_span, tmp_size);
    data_span = data_span.subspan(tmp_size);
    type_data_size += tmp_size;

    res.z = DeserializeRaw<string>(data_span, tmp_size);
    data_span = data_span.subspan(tmp_size);
    type_data_size += tmp_size;


    return res;
}


struct B
{
    int32_t x = -1;
    A y;
};

void SerializeRaw(B const& val, vector<byte>& data)
{
    SerializeRaw(val.x, data);
    SerializeRaw(val.y, data);
}

template<>
B DeserializeRaw<B>(std::span<const byte>const& data, int64_t& type_data_size)
{
    auto data_span = data;
    int64_t tmp_size;
    type_data_size = 0;
    B res;

    res.x = DeserializeRaw<int32_t>(data_span, tmp_size);
    data_span = data_span.subspan(tmp_size);
    type_data_size += tmp_size;

    res.y = DeserializeRaw<A>(data_span, tmp_size);
    data_span = data_span.subspan(tmp_size);
    type_data_size += tmp_size;

    return res;
}


// Serializer




class EpicSerializer
{
public:

    EpicSerializer()
    {
        RegisterType<A>();
        RegisterType<B>();
    }

    static auto Get()
    {
        static EpicSerializer i;
        return &i;
    }

    template<typename T>
    int32_t GetTypeId()
    {
        if (auto it = _types_map.find(typeid(T)); it != _types_map.end())
            return it->second.id;
        return 0;
    }

    template<typename T>
    void Serialize(T const& val, vector<byte>& data)
    {
        auto it = _types_map.find(typeid(T));
        if (it == _types_map.end())
            throw "Unknown type serialize";

        SerializeRaw(it->second.id, data);
        it->second.serialize((void*)&val, data);
    }

    vector<any> Deserialize(vector<byte> const& data)
    {
        vector<any> res;
        size_t pos = 0;
        while (pos < data.size())
        {
            if (pos + sizeof(uint32_t) > data.size())
                throw "Deserialize wrong";
            span<const byte> uid_data(data.data() + pos, sizeof(uint32_t));
            int64_t data_type_size;
            auto uid = DeserializeRaw<int32_t>(uid_data, data_type_size);
            pos += data_type_size;

            auto it = _types_uid_map.find(uid);
            if (it == _types_uid_map.end())
                throw "Deserialize wrong";

            span<const byte> t1{ data.data() + pos, data.size() - pos };

            auto& cell = _types_map[it->second];
            res.push_back(cell.deserialize(t1, data_type_size));
            pos += data_type_size;
        }
        return res;
    }


private:

    struct Cell
    {
        int32_t id = 0;
        function<void(void* val, vector<byte>& data)> serialize;
        function<any(std::span<const byte>const& data, int64_t& type_data_size)> deserialize;
    };

    template<typename T>
    static void InternalSerialize(void* val, vector<byte>& data)
    {
        SerializeRaw(*(T*)val, data);
    }
    template<typename T>
    static any InternalDeserialize(std::span<const byte>const& data, int64_t& type_data_size)
    {
        return DeserializeRaw<T>(data, type_data_size);
    }

    template<typename T>
    void RegisterType()
    {
        int32_t uid = _types_map.size() + 1;

        Cell cell{
        .id = uid,
        .serialize = &InternalSerialize<T>,
        .deserialize = &InternalDeserialize<T> };

        assert(_types_map.find(typeid(T)) == _types_map.end());
        _types_map.insert({ typeid(T), cell });
        _types_uid_map.insert({ uid, typeid(T) });
    }



    unordered_map<type_index, Cell> _types_map;
    unordered_map<int32_t, type_index> _types_uid_map;
};










int main()
{
    A a;
    B b;

    vector<byte> data;
    EpicSerializer::Get()->Serialize(a, data);
    EpicSerializer::Get()->Serialize(b, data);

    auto vec = EpicSerializer::Get()->Deserialize(data);

    A a1 = any_cast<A>(vec[0]);
    B b1 = any_cast<B>(vec[1]);

    return 0;
}

В данном варианте не учитывается много чего. Например изменение полей типов, big-little endian итд. Реальное решение задачи сереализации гораздо сложнее(пока в C++ не завезли рефлексию).

→ Ссылка
Автор решения: eri

Для массива структур в лоб нужно указатели char* obj; заменить на поля определенной длинны. Например char [30] obj. В этом случае структура будет содержать в себе все данные, хоть и ограниченные по размеру.

И читаться все будет в один read или простым циклом. Хотя можно и мемори мап сделать.

Если нужен перенос на другую архитектуру, то к интам ещё добавьте ntoh/hton

→ Ссылка
Автор решения: Solt

Если сделать массив фиксированным с запасом длины для каждого слова, то mas указывает на непрерывную область с размером sizeof(mas):

const char mas[8][10] = { "Велосипед", "400", "Самокат", "350",
                          "Ролики", "300", "Скейтборд", "200" };
/*и сохранять сразу*/
fout.write(mas, sizeof(mas));

При сохранении можно дописать после этого ещё и символ '\n', тогда считать можно будет сразу через gets();

Та же история со структурой. В нынешнем виде она предполагает, что будут выделяться области памяти под поля типа char* и укладываться в них в виде ссылок. Но, если не жалко немножко байтов, выделить фиксировано:

struct Rent{
    char obj[10]; //Не знаю сколько надо
    char date[10]; // Я бы делал просто таймстемп uint_32t 
                   // или полноценный time_t
    unsigned int beg;
    unsigned int end;
    char name[64]; //"Остап Сулейман Берта Мария Бендер Бей" влазит в 37
};
/*И вот тогда sizeof отработает корректно*/
fout.write(reinterpret_cast<char*>(arr), n * sizeof(Rent));
→ Ссылка
Автор решения: Serge3leo

Запись и чтение из файла этих структур, увы, по ряду причин требует переработки, как их самих, так и кода доступа к ним, в частности:

  • Поле obj может указывать куда угодно: статические данные, память полученная malloc()/strdup(), память полученная new или память полученная new char[];
  • При чтении содержимое массива const char* mas[] будет затруднительно разместить в сегменте только для чтения;
  • ...

Вопрос в минимизации этих изменений. С точки зрения размера примера, наверное минимальным будет что-то в духе:

// Boost C++ Serialization (simple text format)
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
#include <boost/serialization/vector.hpp>

struct Rent {
    std::string obj;
    std::string date;
    unsigned int beg;
    unsigned int end;
    std::string name;
    template<class Archive>
    void serialize(Archive& ar, const unsigned int /*version*/) {
        ar & obj & date & beg & end & name;
    }
};
int main() {
    const Rent test = {"Arthur", "29 March 1977", 42, 54, "Deep Thought"};
    const std::vector<Rent> test_vr = { test, test };
    const std::vector<std::string> test_vs = {
            "Велосипед", "400", "Самокат", "350",
            "Ролики", "300", "Скейтборд", "200"
        };
    {
        const auto r = test;
        const auto vr = test_vr;
        const auto vs = test_vs;
        std::ofstream ofs("filename");
        boost::archive::text_oarchive oa(ofs);
        oa << r;
        oa << vr;
        oa << vs;
        std::cout << "Запись хорь\n";
    }
    {
        Rent newr;
        std::vector<Rent> newvr;
        std::vector<std::string> newvs;
        std::ifstream ifs("filename");
        boost::archive::text_iarchive ia(ifs);
        ia >> newr;
        ia >> newvr;
        ia >> newvs;
        assert(newr.name == test.name && newr.end == test.end);
        assert(newvr.size() == test_vr.size());
        assert(newvs.size() == test_vs.size() && newvs[4] == test_vs[4]);
        std::cout << "Чтение хорь\n";
    }
}

Работу примера смотрите: https://godbolt.org/z/a1ojhMaah

Конечно, он немного длиннее варианта @Solt, но там опущены необходимые определения для максимальных длин полей и т.д. Зато такой же общий, как вариант @Никита Самоуков, и существенно его короче.

Для простоты используются std::string и std::vector, ввиду наличия стандартных конструкторов и деструкторов, но, при необходимости, их можно заменить на char * и массивы при условии формализации их создания, удаления, копирования или перемещения. Упрощённый вариант логики конструкторов/деструкторов для нуль-терминированных char *: https://godbolt.org/z/1dGqo931a

→ Ссылка