Есть ли способ сериализовать гетерогенный вектор с помощью библиотеки nlohman_json?

#c #inheritance #stdvector #unique-ptr #nlohmann-json

#c #наследование #stdvector #уникальный-ptr #nlohmann-json

Вопрос:

Привет сообществу переполнения стека!

Я работаю над проектом, в котором активно используется интересная nlohmann_json библиотека, и кажется, что мне нужно добавить ссылку наследования для определенного класса, объекты которого сериализуются в один момент.

Я пробовал разные советы, найденные на странице github Issues библиотеки, но не могу заставить его работать.

Вот фиктивный код, который я пробовал :

 #include <nlohmann/json.hpp>

#include <iostream>
#include <memory>
#include <vector>

using json = nlohmann::json;

namespace nlohmann {
    template <typename T>
    struct adl_serializer<std::unique_ptr<T>> {
        static void to_json(jsonamp; j, const std::unique_ptr<T>amp; opt) {
            if (opt) {
                j = *opt.get();
            } else {
                j = nullptr;
            }
        }
    };
}

class Base {
    public:
        Base() = default;
        virtual ~Base() = default;
        virtual void foo() const { std::cout << "Base::foo()" << std::endl; }
};

class Obj : public Base
{
    public:
        Obj(int i) : _i(i) {}
        void foo() const override { std::cout << "Obj::foo()" << std::endl; }
        int _i = 0;
        friend std::ostreamamp; operator<<(std::ostreamamp; os, const Objamp; o);
};

std::ostreamamp; operator<<(std::ostreamamp; os, const Baseamp; o)
{
    os << "Base{} ";
    return os;
}

std::ostreamamp; operator<<(std::ostreamamp; os, const Objamp; o)
{
    os << "Obj{"<< o._i <<"} ";
    return os;
}

void to_json(jsonamp; j, const Baseamp; b)
{
    std::cout << "called to_json for Base" << std::endl;
}

void to_json(jsonamp; j, const Objamp; o)
{
    std::cout << "called to_json for Obj" << std::endl;
}

int main()
{
    std::vector<std::unique_ptr<Base>> v;
    v.push_back(std::make_unique<Base>());
    v.push_back(std::make_unique<Obj>(5));
    v.push_back(std::make_unique<Base>());
    v.push_back(std::make_unique<Obj>(10));

    std::cout << v.size() << std::endl;

    json j = v;
}
// Results in :
// Program returned: 0
// 4
// called to_json for Base
// called to_json for Base
// called to_json for Base
// called to_json for Base
  

(https://gcc.godbolt.org/z/dc8h8f )

Я понимаю, что adl_serializer единственный получает тип Base при вызове, но я не вижу, как заставить его также знать о типе Obj

Кто-нибудь видит, чего мне здесь не хватает?

Заранее спасибо за ваши советы и помощь!

Комментарии:

1. Вам придется реализовать свой собственный полиморфный слой, например, путем сохранения дополнительного type поля внутри JSON и проверки его значения при десериализации.

2. @Quentin Я понимаю, что вы говорите об управлении полиморфизмом внутри файла, сформированного в формате JSON, при де- / сериализации, и полностью согласен с этим. Но здесь проблема в том, что я не могу сериализовать свой вектор, содержащий полиморфные объекты. Я не знаю, почему или как adl_serializer вызвать функцию void to_json(jsonamp; j, const Objamp; o) for Obj -typed objects instead of the function void to_json(jsonamp; j, const Baseamp; b)`.

3. Я не вижу виртуального деструктора. Плохо.

4. @AsteroidsWithWings Да, вы правы, так и должно быть. Это просто фиктивный код сам по себе, но вы правы 🙂

5. Фиктивный код или нет, у вас неопределенное поведение, и (хотя, я думаю, мы исключили это, учитывая принятый ответ ниже) подобные вещи могут привести к той проблеме, с которой вы столкнулись 🙂 Никогда не пропускайте свой виртуальный деструктор.

Ответ №1:

nlohmann.json не включает полиморфную сериализацию, но вы можете реализовать ее самостоятельно в специализированной adl_serializer . Здесь мы сохраняем и проверяем дополнительное _type поле JSON, используемое в качестве ключа для сопоставления с парами функций from / to с удаленным типом для каждого производного типа.

 namespace PolymorphicJsonSerializer_impl {
    template <class Base>
    struct Serializer {
        void (*to_json)(json amp;j, Base const amp;o);
        void (*from_json)(json const amp;j, Base amp;o);
    };

    template <class Base, class Derived>
    Serializer<Base> serializerFor() {
        return {
            [](json amp;j, Base const amp;o) {
                return to_json(j, static_cast<Derived const amp;>(o));
            },
            [](json const amp;j, Base amp;o) {
                return from_json(j, static_cast<Derived amp;>(o));
            }
        };
    }
}

template <class Base>
struct PolymorphicJsonSerializer {

    // Maps typeid(x).name() to the from/to serialization functions
    static inline std::unordered_map<
        char const *,
        PolymorphicJsonSerializer_impl::Serializer<Base>
    > _serializers;

    template <class... Derived>
    static void register_types() {
        (_serializers.emplace(
            typeid(Derived).name(),
            PolymorphicJsonSerializer_impl::serializerFor<Base, Derived>()
        ), ...);
    }

    static void to_json(json amp;j, Base const amp;o) {
        char const *typeName = typeid(o).name();
        _serializers.at(typeName).to_json(j, o);
        j["_type"] = typeName;
    }

    static void from_json(json const amp;j, Base amp;o) {
        _serializers.at(j.at("_type").get<std::string>().c_str()).from_json(j, o);
    }
};
  

Использование:

 // Register the polymorphic serializer for objects derived from `Base`
namespace nlohmann {
    template <>
    struct adl_serializer<Base>
        : PolymorphicJsonSerializer<Base> { };
}

// Implement `Base`'s from/to functions
void to_json(json amp;, Base const amp;) { /* ... */ }
void from_json(json const amp;, Base amp;) { /* ... */ }


// Later, implement `Obj`'s from/to functions
void to_json(json amp;, Obj const amp;) { /* ... */ }
void from_json(json const amp;, Obj amp;) { /* ... */ }

// Before any serializing/deserializing of objects derived from `Base`, call the registering function for all known types.
PolymorphicJsonSerializer<Base>::register_types<Base, Obj>();

// Works!
json j = v;
  

Предостережения:

  • typeid(o).name() является уникальным на практике, но не гарантируется стандартом. Если это проблема, ее можно заменить любым постоянным методом идентификации типа среды выполнения.

  • Обработка ошибок не учитывалась, хотя _serializers.at() std::out_of_range при попытке сериализовать неизвестный тип будет возникать ошибка.

  • Эта реализация требует, чтобы Base тип реализовал свою сериализацию с from/to помощью функций ADL, поскольку он берет верх nlohmann::adl_serializer<Base> .

Посмотрите это в прямом эфире на Wandbox

Комментарии:

1. Это было абсолютно не то, что я понимаю, если ваш первый комментарий, мои извинения… и это блестяще! Я не думал об этом. Это не очевидно, так что, может быть, даже вне лиги на данный момент, и за это вам большое спасибо!

2. Не было бы лучше использовать std::string для ключей на карте вместо const char * ? В моем понимании поиск при использовании const char * сравнивал бы указатели, а не значения. Кроме того, это может быть причиной того, что приведенный выше пример не работает при десериализации значений.

3.@Cereaubra это то, что typeid(o).name() возвращается, и, насколько я могу судить, оно должно представлять собой достаточно хороший уникальный идентификатор. Можете ли вы подробнее рассказать о том, что не работает?

4.При изменении вашего wandbox.com пример, чтобы также десериализовать исключение, генерируется: terminate called after throwing an instance of 'std::out_of_range'. what(): _Map_base::at . Я добавил следующие три строки: Base b1 = *v[0].get(); json j_b1 = b1; Base re_b1 = j_b1; Исключение генерируется при поиске из _serializers via j.at("_type")

5. Это хорошо работает для преобразования в json объект, но как преобразовать обратно в vector работу? Нужно ли нам писать фабричную функцию, которая считывает _type свойство и создает соответствующий подкласс перед вызовом *sub_ptr = j.get<Obj> ?