#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>
.
Комментарии:
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
viaj.at("_type")
5. Это хорошо работает для преобразования в
json
объект, но как преобразовать обратно вvector
работу? Нужно ли нам писать фабричную функцию, которая считывает_type
свойство и создает соответствующий подкласс перед вызовом*sub_ptr = j.get<Obj>
?