14.9. Применение XML для сохранения и восстановления набора объектов

14.9. Применение XML для сохранения и восстановления набора объектов

Проблема

Требуется иметь возможность сохранения набора объектов C++ в документе XML и считывания их потом обратно в память.

Решение

Используйте библиотеку Boost Serialization. Эта библиотека позволяет сохранять и восстанавливать объекты, используя классы, называемые архивами. Для использования этой библиотеки вы должны сначала сделать каждый из ваших классов сериализуемым (serializable), что просто означает возможность записи экземпляров класса в архив (это называется сериализацией) и их обратного считывания в память (это называется десериализацией). Затем на этапе выполнения вы можете сохранить ваши объекты в архиве XML, используя оператор <<, и восстановить их, используя оператор >>.

Чтобы сделать класс сериализуемым, добавьте шаблон функции-члена serialize со следующей сигнатурой.

template<typename Archive>

void serialize(Archive& ar, const unsigned int version);

В реализации serialize необходимо обеспечить запись каждого данного-члена класса в указанный архив в виде пары «имя-значение», используя оператор &. Например, если вы хотите сериализовать и десериализовать экземпляры класса Contact из примера 14.2, добавьте функцию-член serialize, как это сделано в примере 14.25.

Пример 14.25. Добавление поддержки сериализации в класс Contact из примера 14.2

#include <boost/serialization/nvp.hpp> // пара "имя-значение"

class Contact {

 ...

private:

 friend class boost::serialization::access;

 template<typename Archive>

 void serialize(Archive& ar, const unsigned int version) {

  // Записать (или считать) каждое данное-член в виде пары имя-значение

  using boost::serialization::make_nvp;

  ar & make_nvp("name", name_);

  ar & make_nvp("phone", phone_);

 }

...

};

Аналогично можно обеспечить сериализацию класса Animal из примера 14.2, как это сделано в примере 14.26.

Пример 14.26. Добавление поддержки сериализации для класса Animal из примера 14.2

...

// Включить поддержку сериализации для boost::gregorian::date

#include <boost/date_time/gregorian/greg_serialize.hpp>

...

class Contact {

 ...

private:

 friend class boost::serialization::access;

 template<typename Archive>

 void serialize(Archive& ar, const unsigned int version) {

  // Записать (или считать) каждое данное-член в виде пары имя-значение

  using boost::serialization::make_nvp;

  ar & make_nvp("name", name_);

  ar & make_nvp("species", species_);

  ar & make_nvp("dateOfBirth", dob_);

  ar & make_nvp("veterinarian", vet_);

  ar & make_nvp("trainer", trainer_);

 }

 ...

};

Теперь вы можете сериализовать Animal, создавая архив XML типа boost::archive::xml_oarchive и записывая информацию о животном в архив, используя оператор <<. Конструктор xml_oarchive в качестве аргумента принимает std::ostream; часто этим аргументом будет поток вывода, используемый для записи в файл, однако в общем случае для записи данных может использоваться ресурс любого типа. После сериализации экземпляра Animal его можно считать обратно в память, конструируя архив XML типа boost::archive::xml_iarchive, подключая его к тому же самому ресурсу, который использовался первым архивом, и применяя оператор >>.

Пример 14.27 показывает, как можно использовать Boost.Serialization для сохранения вектора std::vector, состоящего из объектов Animal, в файле animals.xml и затем для загрузки его обратно в память. В примере 14.28 показано содержимое файла animals.xml после выполнения программы из примера 14.27.

Пример 14.27 Сериализация вектора std::vector, состоящего из объектов Animal

#include <fstream>

#include <boost/archive/xml_oarchive.hpp> // Архив для записи XML

#include <boost/archive/xml_iarchive.hpp> // Архив для чтения XML

#include <boost/serialization/vector.hpp> // Средства сериализации вектора

#include "animal.hpp" // std::vector

int main() {

 using namespace std;

 using namespace boost::archive;       // пространство имен для архивов

 using namespace boost::serialization; // пространство имен для make_nvp

 try {

  // Заполнить список животных

  vector<Animal> animalList;

  animalList.push_back(

   Animal("Herby", "elephant", "1992-04-23",

   Contact("Dr. Hal Brown", "(801)595-9627"),

   Contact("Bob Fisk", "(801)881-2260")));

  animalList.push_back(

   Animal("Sheldon", "parrot", "1998-09-30",

   Contact("Dr. Kevin Wilson", "(801)466-6498"),

   Contact("Eli Wendel", "(801)929-2506")));

  animalList.push_pack(

   Animal("Dippy", "penguin", "2001-06-08",

   Contact("Dr. Barbara Swayne", "(801)459-7746"),

   Contact("Ben Waxman", "(801)882-3549")));

  // Сконструировать выходной архив XML и сериализовать список

  ofstream fout("animals.xml");

  xml_oarchive oa(fout);

  oa << make_nvp("animalList", animalList);

  fout.close();

  // Сконструировать входной архив XML и десериализовать список

  ifstream fin("animals.xml");

  xml_iarchive ia(fin);

  vector<Animal> animalListCopy;

  ia >> make_nvp("animalList", animalListCopy);

  fin.close();

  if (animalListCopy != animalList) {

   cout << "XML serialization failed ";

   return EXIT_FAILURE;

  }

 } catch (const exception& e) {

  cout << e.what() << " ";

  return EXIT_FAILURE;

 }

}

Пример 14.28. Файл animals.xml после выполнения программы из примера 14.27

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>

<!DOCTYPE boost_serialization>

<boost_serialization signature="serialization::archive" version="3">

<animalList class_id="0" tracking_level ="0" version="0">

 <count>3</count>

 <item class_id="1" tracking_level="0" version="0">

  <name>Herby</name>

  <species>elephant</species>

  <dateOfBirth class_id="2" tracking_level="0" version="0">

   <date>19920423</date>

  </dateOfBirth>

  <veterinarian class_id="3" tracking_level="0" version="0">

   <name>Dr. Hal Brown</name>

   <phone>(801)595-9627</phone>

  </veterinarian>

  <trainer>

   <name>Bob Fisk</name>

   <phone>(801)881-2260</phone>

  </trainer>

 </item>

 <item>

  <name>Sheldon</name>

  <species>parrot</species>

  <dateOfBirth>

   <date>19980930</date>

  </dateOfBirth>

  <veterinarian>

   <name>Dr. Kevin Wilson</name>

   <phone>(801)466-6498</phone>

  </veterinarian>

  <trainer>

   <name>Eli Wendel</name>

   <phone>(801)929-2506</phone>

  </trainer>

 </item>

 <item>

  <name>Dippy</name>

  <species>penguin</species>

  <dateOfBirth>

   <date>20010608</date>

  </dateOfBirth>

  <veterinarian>

   <name>Dr. Barbara Swayne</name>

   <phone>(801)459-7746</phone>

  </veterinarian>

  <trainer>

   <name>Ben Waxman</name>

   <phone>(801)882-3549</phone>

  </trainer>

 </item>

</animalList>

Обсуждение

Библиотека Boost Serialization обеспечивает наиболее изощренный и гибкий способ сохранения и восстановления объектов C++. Она представляет собой очень сложный фреймворк. Например, она позволяет сериализовать сложные структуры данных, содержащие циклические ссылки и указатели на полиморфные объекты. Более того, применение этой библиотеки совсем не ограничивается сериализацией XML: кроме архивов XML она предоставляет несколько типов текстовых и бинарных архивов. Архивы XML и текстовые архивы являются переносимыми, т.е. данные можно сериализовать в одной системе и десериализовать в другой; бинарные архивы не переносимы, но компактны.

Нет никаких спецификаций, которым соответствовали бы документы XML, полученные при помощи Boost.Serialization, и их формат может изменяться в новых версиях Boost. Поэтому вы не можете использовать эти документы совместно с другими фреймворками сериализации С++. Тем не менее XML-сериализация приносит пользу, потому что сериализованный вывод легко воспринимается человеком и может обрабатываться инструментальными средствами, ориентированными на XML.

Примеры 14.25 и 14.26 демонстрируют интрузивную сериализацию (intrusive serialization): классы Animal и Contact были модифицированы, чтобы обеспечить их сериализацию. Boost.Serialization также поддерживает неинтрузивную сериализацию (nonintrusive serialization), обеспечивая сериализацию классов без модификации их определений при условии доступности всех состояний объекта через его открытый интерфейс. Вы уже видели пример неинтрузивной сериализации в примере 14.27: шаблон std::vector допускает сериализацию, несмотря на то что его определение не может модифицироваться конечными пользователями. Фактически все контейнеры стандартной библиотеки являются сериализуемыми; для обеспечения сериализации контейнера, определенного в стандартном заголовочном файле xxx, просто включите заголовочный файл boost/serialization/xxx.hpp. Дополнительную информацию о неинтрузивной сериализации вы можете найти в документации Boost.Serialization.

Примеры 14.25 и 14.26 иллюстрируют также двойственную роль оператора &: он действует как оператор << при сериализации объекта и как оператор >> при десериализации объекта. Это удобно, потому что позволяет реализовать сериализацию и десериализацию одной функцией. Однако в некоторых случаях неудобно использовать одну функцию для сериализации и десериализации; для этого в Boost.Serialization предусмотрен механизм разделения метода serialize() на два отдельных метода, load() и save(). Если вам необходимо воспользоваться преимуществами этой возможности, обратитесь к документации Boost.Serialization.

В примерах 14.25, 14.26 и 14.27 я использую функцию boost::serialization::make_nvp для конструирования пар вида «имя-значение». В Boost.Serialization предусмотрен также макрос BOOST_SERIALIZATION_NVP, который позволяет выполнять сериализацию переменной, указывая ее имя. Первый компонент пары будет сконструирован автоматически препроцессором, используя оператор «стрингизации» (stringizing) # для преобразования макропараметров в строковые константы.

// То же самое, что и ar & make_nvp("name_", name_);

ar & BOOST_SERIALIZATION_NVP(name_);

В этих примерах я использую make_nvp вместо BOOST_SERIALIZATION_NVP для лучшего контроля имен тегов, чтобы содержимое архива XML легче читалось.

В документации Boost.Serialization рекомендуется объявлять метод serialize() как закрытый (private) для уменьшения ошибок пользователя, когда добавляется поддержка сериализации в классы, производные от других сериализуемых классов. Для того чтобы библиотека Boost.Serialization могла вызвать метод serialize() вашего класса, вам необходимо объявить дружественным класс boost::serialization::access.

Наконец второй параметр метода serialize() в примерах 14.25 и 14.26 относится к той части Boost.Serialization, которая поддерживает управление версиями классов (class versioning). Когда объект определенного класса первый раз сохраняется в архиве, вместе с ним сохраняется также его версия; когда выполняется десериализация экземпляра класса. Boost.Serialization передает сохраненную версию методу serialize в качестве второго аргумента. Эта информация может использоваться для специализации десериализации; например, serialize мог бы загружать переменную-член только в том случае, если записанная в архив версия класса, по крайней мере, не меньше версии класса, первым объявившим эту переменную. По умолчанию класс имеет версию 0. Для задания версии класса вызовите макрос BOOST_CLASS_VERSION, который определен в заголовочном файле boost/serialization/version.hpp, передавая в качестве аргументов имя и версию класса.