Есть ли способ указать более простую JSON (де-) сериализацию для std::map, используя Cereal / C++?
Проект, над которым я работаю, представляет собой приложение C++, которое управляет большим количеством пользовательских аппаратных устройств. Приложение имеет интерфейс сокета / порта для клиента (например, графический интерфейс). У каждого типа устройства есть своя четко определенная схема JSON, и мы можем сериализовать их с помощью Cereal.
Но приложению также необходимо проанализировать входящие JSON-запросы от клиента. Одна часть запроса задает параметры фильтра устройства, примерно аналогично предложению SQL WHERE, в котором все выражения объединены в AND. Например:
"filter": { "type": "sensor", "status": "critical" }
Это указывает на то, что клиент хочет выполнить операцию на каждом "сенсорном" устройстве с "критическим" состоянием. На первый взгляд казалось, что реализация C++ для параметров фильтра будет std::map. Но когда мы экспериментировали с использованием Cereal для десериализации объекта, это не удалось. И когда мы сериализуем жестко закодированную карту фильтра, это выглядит так:
"filter": [
{ "key": "type", "value": "sensor" },
{ "key": "status", "value": "critical" }
]
Теперь я могу понять, почему Cereal поддерживает этот вид многословной сериализации карты. В конце концов, ключ карты может быть нестроковым типом. Но в этом случае ключ является строкой.
Я не очень хочу переписывать нашу спецификацию интерфейса и заставлять наших клиентов генерировать явно не-идиоматический JSON просто для удовлетворения Cereal. Я новичок в Cereal, и мы застряли в этом вопросе. Есть ли способ сказать Cereal для анализа этого фильтра как std::map? Или, может быть, я спрашиваю это неправильно. Есть ли какой-нибудь другой контейнер stl, в который мы должны десериализоваться?
1 ответ
Позвольте мне сначала рассказать, почему хлопья дают более многословный стиль, чем тот, который вы можете пожелать. cereal написан для работы с произвольными архивами сериализации и использует промежуточный подход для их удовлетворения. Представьте, что тип ключа является чем-то более сложным, чем строковый или арифметический тип - как мы можем сериализовать его простым "key" : "value"
путь?
Также обратите внимание, что зерновые ожидают быть прародителем любых данных, которые они считывают.
Это, как говорится, то, что вы хотите, вполне возможно с хлопьями, но есть несколько препятствий:
Самым большим препятствием, которое необходимо преодолеть, является тот факт, что желаемый входной поток сериализует некоторое неизвестное количество пар имя-значение внутри объекта JSON, а не массива JSON. cereal был разработан для использования массивов JSON при работе с контейнерами, которые могут содержать переменное число элементов, поскольку это имело наибольшее значение с учетом используемого им базового анализатора rapidjson.
Во-вторых, зерновые в настоящее время не ожидают, что имя в паре имя-значение будет фактически загружено в память - они просто используют их в качестве организационного инструмента.
Таким образом, мы можем быстро разобраться, вот полностью работающее решение (может быть сделано более элегантно) для вашей проблемы с минимальными изменениями для зерновых (на самом деле используется изменение, которое запланировано для зерновых 1.1, текущая версия - 1.0):
Добавить эту функцию в JSONInputArchive
:
//! Retrieves the current node name
/*! @return nullptr if no name exists */
const char * getNodeName() const
{
return itsIteratorStack.back().name();
}
Затем вы можете написать специализацию сериализации для std::map
(или неупорядоченный, в зависимости от того, что вы предпочитаете) для пары строк. Убедитесь, что поместили это в cereal
пространство имен, чтобы его мог найти компилятор. Этот код должен существовать где-то в ваших собственных файлах:
namespace cereal
{
//! Saving for std::map<std::string, std::string>
template <class Archive, class C, class A> inline
void save( Archive & ar, std::map<std::string, std::string, C, A> const & map )
{
for( const auto & i : map )
ar( cereal::make_nvp( i.first, i.second ) );
}
//! Loading for std::map<std::string, std::string>
template <class Archive, class C, class A> inline
void load( Archive & ar, std::map<std::string, std::string, C, A> & map )
{
map.clear();
auto hint = map.begin();
while( true )
{
const auto namePtr = ar.getNodeName();
if( !namePtr )
break;
std::string key = namePtr;
std::string value; ar( value );
hint = map.emplace_hint( hint, std::move( key ), std::move( value ) );
}
}
} // namespace cereal
Это не самое элегантное решение, но оно работает хорошо. Я оставил все шаблонно, но то, что я написал выше, будет работать только с архивами JSON с учетом внесенных изменений. Добавление аналога getNodeName()
к XML-архиву, скорее всего, и там будет работать, но, очевидно, это не имеет смысла для двоичных архивов.
Чтобы сделать это чистым, вы хотели бы поставить enable_if
вокруг этого для архивов это работает с. Вам также необходимо изменить архивы JSON в зерновых для работы с объектами JSON переменного размера. Чтобы получить представление о том, как это сделать, посмотрите, как злаки устанавливают состояние в архиве, когда получают SizeTag
сериализовать. По сути, вам нужно заставить архив не открывать массив, а вместо этого открывать объект, а затем создавать свою собственную версию loadSize()
что бы увидеть насколько велик объект (это было бы Member
на быстром языке).
Чтобы увидеть вышеизложенное в действии, запустите этот код:
int main()
{
std::stringstream ss;
{
cereal::JSONOutputArchive ar(ss);
std::map<std::string, std::string> filter = {{"type", "sensor"}, {"status", "critical"}};
ar( CEREAL_NVP(filter) );
}
std::cout << ss.str() << std::endl;
{
cereal::JSONInputArchive ar(ss);
cereal::JSONOutputArchive ar2(std::cout);
std::map<std::string, std::string> filter;
ar( CEREAL_NVP(filter) );
ar2( CEREAL_NVP(filter) );
}
std::cout << std::endl;
return 0;
}
и вы получите:
{
"filter": {
"status": "critical",
"type": "sensor"
}
}
{
"filter": {
"status": "critical",
"type": "sensor"
}
}