Сообщений 39    Оценка 1785 [+0/-1]         Оценить  
Система Orphus

C++: метаданные своими руками

Чтение/запись простых Xml-файлов из программы на классическом С++

Автор: Андрей Мартынов
The RSDN Group

Источник: RSDN Magazine #5-2003
Опубликовано: 30.11.2003
Исправлено: 10.12.2016
Версия текста: 1.0
Проблема
Решение
Подробности
«Деревянное» хранилище
Registry
Метаклассы
Разметка структуры
Разбор (парсинг) примитивов
Метаданные своими руками
Конфигурационные файлы .Net
Заключение

Исходные тексты примера

Проблема

Прежде всего, надо пояснить подзаголовок. «Классический С++» здесь означает «не MC++», то есть не C++ with managed extensions. Это важное уточнение, так как работа с XML в среде .Net – это лёгкое и приятное занятие. В этой среде имеются встроенные средства сериализации типов, основанные на метаданных. Но что делать, если вы пишете на старом («классическом») С++, а читать/писать xml-файлы вам нужно не в меньшей степени, чем коллегам, пишущим на .Net-совместимых языках? Вот этой цели – скрасить жизнь С++ программистов, работающих с XML, – и посвящена библиотека классов (шаблонов), которая будет представлена ниже.

Второе уточнение подзаголовка статьи – что означает выражение «простые Xml-файлы»? Здесь «простота» подразумевает, что до полного повторения всех возможностей, имеющихся в .Net, дело не дошло (пока), решена лишь задача, описание которой приведено ниже.

Имеется структура данных, содержащая подструктуры любой степени вложенности, а также массивы. И поля структур, и элементы массивов имеют типы-значения, т.е. среди них нет ни указателей, ни ссылок. Задача состоит в том, чтобы обеспечить универсальный алгоритм чтения/записи такой структуры в виде xml-файла. Изменение структуры данных не должно приводить к перепрограммированию алгоритма чтения. Чтение должно быть основано на неких данных, которыми предварительно снабжены (размечены) все типы, участвующие в считываемой/записываемой структуре.

Короче, при таком подходе программист не должен для каждого типа сохраняемых данных писать процедуру обхода XMLDOM-дерева. Он лишь декларирует (описывает, размечает) структуру данных специальным образом, чтобы потом использовать универсальный алгоритм чтения/записи. Ровно то, что называется декларативным подходом в программировании.

ПРИМЕЧАНИЕ

Для решения подобных задач в .Net используют атрибуты. Нечто подобное атрибутам придется сделать и в C++, если нам хочется работать в том же стиле.

Решение

Предлагаю для начала, не вдаваясь в подробности устройства библиотеки, посмотреть на то, как она применяется, и что это даёт. Начнём с простейшего. Правило первое – чтобы записать структуру в xml-файл, её необходимо разметить, т.е. сопоставить полям структуры имена xml-тегов, и задать значения полей по умолчанию (если при чтении файла полю разрешено отсутствовать). Ниже приведён пример создания простейшего xml-файла.

      #include
      "SerializeXml.h" // «волшебство» здесь 8-)
      
struct Clr
{
  Clr() : r(0), g(0), b(0) {}
  Clr(byte _r, byte _g, byte _b) : r(_r), g(_g), b(_b) {}
  byte  r;
  short g;
  long  b;
  conststatic Clr white;
  conststatic Clr black;

  struct LayoutDefault : public Layout<Clr>
{
LayoutDefault()
{//        тег        поле   значение по умолчанию
Simple(_T("red"  ), &Clr::r, &Clr::white.r);
Simple(_T("green"), &Clr::g, &Clr::white.g);
Simple(_T("blue" ), &Clr::b, &Clr::white.b);
}
};
};
. . . 
Clr clr(23, 196, 7);
Xml::Save(_T("color.xml"), _T("clr"), clr); // второй параметр – корневой тег

Как вы видите, в тело класса добавлен вложенный тип LayoutDefault, унаследованный от шаблонного класса Layout<Clr>. В конструкторе класса LayoutDefault с помощью специального метода Simple происходит разметка структуры. Этот метод принимает в качестве входных параметров имя тега, указатель на поле и указатель на значение поля по умолчанию (может быть NULL, если нет значения по умолчанию).

Такая разметка структуры позволяет нам использовать метод Xml::Save для сохранения структуры в виде xml-файла. Xml::Save – этот тот самый универсальный метод сохранения данных, о котором говорилось при постановке задачи. Имеется аналогичный метод для чтения данных из Xml-файла – Xml::Load.

Результатом будет следующий файл:

<?xml version="1.0" encoding="utf-8" ?> 
<clr>
  <red>23</red> 
  <green>196</green> 
  <blue>7</blue> 
</clr>

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

      struct LayoutDefault : public Layout<Clr>
{
  LayoutDefault()
  {
    Attribute(_T("red"  ), &Clr::r, &Clr::white.r);
    Attribute(_T("green"), &Clr::g, &Clr::white.g);
    Attribute(_T("blue" ), &Clr::b, &Clr::white.b);
  }
};

Результат получается соответствующий:

<?xml version="1.0" encoding="utf-8" ?> 
<clr red="23" green="193" blue="7" /> 

Просто, не правда ли? А если структура данных будет более сложной? Если будут присутствовать вложенные структуры, массивы? OK, давайте рассмотрим ещё один пример:

      struct Pnt
{
  int               x;
  double            y;
  std::vector<long> vec;
  Clr               color; // Clr из предыдущего примера  struct LayoutDefault : public Layout<Pnt>
  {
    LayoutDefault()
    {
      Simple   (_T("x"    ), &Pnt::x,     &defaultX  );
      Attribute(_T("y"    ), &Pnt::y,     &defaultY  );
      Complex  (_T("color"), &Pnt::color, &Clr::white);
      Array    (_T("vec"  ), &Pnt::vec,   _T("item" ));
    }  //       тег массива                 тег его элемента 
  };
};
. . . 
Pnt pnt;
pnt.x = 4; 
pnt.y = 3.1415; 
pnt.color = Clr::white;
pnt.vec.push_back(8); 
pnt.vec.push_back(16); 
Xml::Save(_T("color.xml"), _T("Pnt"), pnt);

Думаю, если привести результирующий файл, то объяснения будут излишни.

<?xml version="1.0" encoding="utf-8" ?> 
<Pnt y="3.1415">
  <x>4</x> 
  <color red="255" green="255" blue="255" /> 
  <vec>
    <item>8</item> 
    <item>16</item> 
  </vec>
</Pnt>

Итак, общее представление о возможностях библиотеки вы, надеюсь, получили. Осталось только рассказать, как работать с таким важным типом данных, как перечисления. Это тоже совсем не сложно:

      enum DayOfWeek
{
  Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday 
};

struct DayOfWeek_EnumMeta : publicEnumMeta<DayOfWeek> 
{
  DayOfWeek_EnumMeta()
  {
    Enumerator(_T("Sunday"   ), Sunday   );
    Enumerator(_T("Monday"   ), Monday   );
    Enumerator(_T("Tuesday"  ), Tuesday  );
    Enumerator(_T("Wednesday"), Wednesday);
    Enumerator(_T("Thursday" ), Thursday );
    Enumerator(_T("Friday"   ), Friday   );
    Enumerator(_T("Saturday" ), Saturday );
  }
};

ENUM_METADATA(DayOfWeek, DayOfWeek_EnumMeta);// Без макроса не обойтись 8-(

После этих действий перечисление можно использовать при разметке структур, в которые оно входит в качестве поля (как Simple или как Attribute).

      struct Day
{
  Day(){}
  Day(DayOfWeek a_dow, bool a_holiday) : dow(a_dow), holiday(a_holiday) {}

  bool      holiday;
  DayOfWeek dow;  

  conststaticbool defaultHoliday = true;
  conststatic DayOfWeek defaultDOW = Sunday;

  struct LayoutDefault : public Layout<Day>
  {
    LayoutDefault()
    {
      Simple (_T("Holiday"   ), &Day::holiday, &defaultHoliday);
      Simple (_T("DayOfWeek" ), &Day::dow    , &defaultDOW    );
    }
  };
};
. . .
Day day(Sunday, true);
Xml::Save(_T("day.xml"), _T("Day"), day);

Соответствующий xml-файл будет таким:

<?xml version="1.0" encoding="utf-8" ?> 
<Day>
  <Holiday>true</Holiday> 
  <DayOfWeek>Sunday</DayOfWeek> 
</Day>

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

Подробности

В принципе, можно и не вникать в устройство библиотеки. Приведённых примеров достаточно, чтобы по аналогии с ними решать простейшие задачи. Однако лучше всё же заглянуть «под капот». Это пригодится, если будет сделано что-то не так, и компилятор выдаст какое-то непонятное диагностическое сообщение. Стоит предупредить, при работе с библиотекой шаблонов расшифровка сообщений компилятора бывает порой делом весьма сложным. Знание деталей тут совсем не помешает. А может быть, кто-то захочет усовершенствовать библиотеку, дополнить её новыми возможностями. Надеюсь, что в этом случае сказанное ниже поможет вам лучше сориентироваться.

СОВЕТ

Читать дальнейшее лучше всего, изучая на экране монитора демонстрационный проект, загруженный в среду Visual Studio.

«Деревянное» хранилище

В самом начале разработки библиотеки было принято решение не ограничиваться работой только с XML-файлами. Ведь на свете есть много иерархически устроенных хранилищ данных, и работу с ними можно организовать единообразно. Ниже приведён интерфейс, который был принят для описания хранилища данных в виде дерева, «деревянного» хранилища:

interface INamedNodeList
{
  virtual ~INamedNodeList(){}
  virtual INode* GetByName(const tstring& name) const = 0;
};

interface INodeArray
{
  virtual ~INodeArray(){}
  virtuallong   Count() const = 0;
  virtual INode* GetByIndex(long index) const = 0;
};

interface INode // Абстрактная ветка (элемент или атрибут)
{
  virtual ~INode(){}
  virtual tstring ReadText() const = 0;
  virtualvoid WriteText(const tstring&) = 0;

  virtual INodeArray*     ChildElements(const tstring& tag) const = 0;
  virtual INamedNodeList* ChildElements() const = 0;
  virtual INamedNodeList* Attributes() const = 0;

  virtual INode* AddElement  (const tstring& tag, long index = -1) = 0;
  virtual INode* AddAttribute(const tstring& name) = 0;
};

Используя XMLDOM, эти интерфейсы реализовать легко, тем более что названия операторов специально сделаны похожими на названия операторов Msxml2.DOM. Не стоит тратить здесь время на реализацию этих интерфейсов, так как она довольно проста.

Подчеркну, что эти интерфейсы можно реализовать для хранения данных как, к примеру, в Registry, так и в других «деревянных» хранилищах. Среди прилагающихся исходных текстов вы найдёте реализацию этих интерфейсов для двух хранилищ (XML-файл и Registry).

Registry

Попробуем для примера сохранить уже знакомую нам структуру Pnt в реестре. Это можно сделать следующим образом:

        #include
        "MSerializerRegistry.h"
. . . 
Pnt pnt2;
. . .
Registry::Save(HKEY_CURRENT_USER, _T("Software\\Rsdn\\XmlCpp\\Pnt"), pnt2);

Вот примерно как будет выглядеть результат (см. рисунок 1):


Рисунок 1. Результат сохранения структуры данных в реестре.

К сожалению, реестр не поддерживает массивы узлов (ключей), поэтому приходится эмулировать массивы при помощи специального вида имён узлов.

ПРЕДУПРЕЖДЕНИЕ

При работе с реестром не забывайте о том, что, в отличие от XML, регистр имён элементов (ключей) и атрибутов (величин) здесь не имеет значения. Это надо учитывать при разметке данных и не допускать имён, отличающихся только регистром.

Метаклассы

Как уже упоминалось, данный вариант библиотеки накладывает определённые ограничения на структуру данных, предназначенных для чтения/записи в xml-файл. Принята следующая модель данных: данные могут быть или примитивными типами, или структурами (наборами полей разных типов), или массивами (последовательностями однотипных элементов). Поля и элементы массивов могут быть данными примитивных типов или же структурами и массивами. Короче, данные – это примитивы, структуры и массивы, состоящие из примитивов, структур и массивов, которые в свою очередь состоят из примитивов, структур и массивов и т.д.

Теперь вам будет легко понять, что такое метакласс. Метакласс – это тип класса (класс класса, тип типа), то есть метакласс – это сущность, которая показывает, к какому семейству структур данных принадлежит данная структура. Это примитив? Это структура (в узком смысле)? Или это массив?

Попробуем выразить эту мысль на языке C++. Метакласс превращается в шаблонный тип (параметр шаблона – сам класс), реализующий следующий интерфейс.

        template <typename Data>
struct MetaClass
{
  virtual ~MetaClass() = 0;
  virtual Data ReadNode (const INode&) const = 0;
  virtualvoid WriteNode(INode*, const Data&) const = 0;
};

Естественно, что типы, принадлежащие разным метаклассам, сохраняются и читаются из "деревянного" хранилища по-разному. Этот факт выражается в том, что имеется три реализации интерфейса MetaClass.

Первый потомок – это примитивный метакласс:

        template <typename PrimType>
struct PrimClassMeta
  : public MetaClass<PrimType>
{
  PrimType ReadNode(const INode& node) const
  {
    return Primitive<PrimType>::Parse(node.ReadText());
  }

  void WriteNode(INode* pNode, const PrimType& prim) const
  {
    pNode->WriteText(Primitive<PrimType>::ToString(prim));
  }
};

Реализация примитивного метакласса и сама по себе примитивна. Этот класс читает текст узла дерева и разбирает его с помощью некоторого «приспособления» – шаблонного класса Primitive. О классе Primitive речь будет идти ниже.

Второй метакласс – векторный.

        template <typename ItemType>
struct VectorClassMeta
  : public MetaClass<std::vector<ItemType> >
{
  VectorClassMeta( ArrayItemName              itemName, 
                  const MetaClass<ItemType>&  itemClassMeta)
    : m_itemName(itemName),
     m_itemClassMeta(itemClassMeta)
  {}
  std::vector<ItemType> ReadNode(const INode& node) const { -skiped- }
  void WriteNode(INode* pNode, const std::vector<ItemType>& d)const {-skiped-}

private:
  ArrayItemName m_itemName; const MetaClass<ItemType>& m_itemClassMeta;
};

Как видите, векторный метакласс для своей работы нуждается в метаклассе своих элементов и в именах тегов своих элементов. Этого ему достаточно, чтобы записать/считать себя в/из xml-файла.

Третий, самый интересный метакласс - структурный.

        template <typename StructType>
struct StructClassMeta
  : public MetaClass<StructType>
{
  StructClassMeta(const Layout<StructType>& layout)
    : m_layout(layout)
  {}
  StructType ReadNode(const INode& node) const 	{ -skiped- }
  void WriteNode(INode* pNode, const StructType& s) const { -skiped- }

protected:
  const Layout<StructType>& m_layout;
};

Напомню, что структура состоит из полей разного типа, расположенных в произвольном порядке. Поэтому для своей работы структурный мета-класс требует полного описания всех типов полей их положения внутри структуры. Вся эта информация хранится в экземпляре шаблонного класса Layout.

Разметка структуры

Layout – это просто набор (массив) данных о каждом поле.

        template <typename StructType>
struct Layout : std::vector<CPtrShared<FieldAttribute<StructType> > > {skiped}

Главную роль в разметке играют шаблонные классы FieldAttribute:

        template <typename StructType>
struct FieldAttrubute : public CRefcounted
{
  virtualvoid ReadField (const INode&, StructType*) = 0;
  virtualvoid WriteField(INode*, const StructType&) = 0;
};

Необходимо пояснить назначение методов ReadField и WriteField. Эти методы читают и пишут не всю структуру, переданную им как параметр (как это делают методы метаклассов), а только одно её поле - то, за которое отвечает данный экземпляр FieldAttribute.

Как вы, наверное, уже догадались, есть три реализации интерфейса FieldAttribute – это поле-примитив, поле-структура, и поле-массив. Приведу реализацию только одного из них – StructFieldAttribute.

Приведённый ниже листинг необходимо прочесть внимательно. Это важно для понимания принципов работы библиотеки.

        template <typename StructType, typename StructFieldType>
struct StructFieldAttrubute : public FieldAttrubute<StructType>
{
  StructFieldAttrubute( FieldName                      name,
                        StructFieldType StructType::*  offset,
                        const StructFieldType*         pDefault = NULL,
                        const Layout<StructFieldType>& layout =
                              DefaultLayout<StructFieldType>())
    : m_layout(layout), m_name(name), m_offset(offset), m_pDefault(pDefault)
  {}

  void ReadField(const INode& node, StructType* pD)
  {
    std::auto_ptr<INamedNodeList> pNodeList (node.ChildElements());
    std::auto_ptr<INode> pNodeChild(pNodeList->GetByName(m_name));
    if (pNodeChild.get() != NULL)
      pD->*m_offset = StructClassMeta<StructFieldType>(m_layout)
                       .ReadNode(*pNodeChild);
    else
    { 
      if (m_pDefault == NULL)
        throw MondatoryFieldException(m_name);
      pD->*m_offset = *m_pDefault;
    }
  }

  void WriteField(INode* pNode, const StructType& d)
  {
    std::auto_ptr<INamedNodeList> pNodeList (pNode->ChildElements());
    std::auto_ptr<INode> pNodeChild(pNodeList->GetByName(m_name));
    if (pNodeChild.get() == NULL)
      pNodeChild.reset(pNode->AddElement(m_name));
    StructClassMeta<StructFieldType>(m_layout)
     .WriteNode(pNodeChild.get(), d.*m_offset);
  }
protected:
  // разметка типа
const Layout<StructFieldType>&      m_layout;   
  // имя тега
  FieldName                           m_name;   
  // положение поля в структуре
  StructFieldType StructType::* const m_offset;
  // значение по умолчанию
const StructFieldType * const       m_pDefault;
};

Обратите внимание на поле m_offset – это указатель на поле в структуре. Он определяет положение (смещение) поля относительно начала структуры. С его помощью производится чтение/запись поля.

Аналогично устроены данные о полях примитива и полях массивах.

Разбор (парсинг) примитивов

Осталось рассказать про примитивы. На самом деле примитивы – это самые важные классы. Всё происходит именно ради них. В конечном счете все цепочки вызовов кончаются чтением или записью строк в тела элементов или в значения атрибутов "деревянного" хранилища.

Примитив (в данном контексте) – это структура данных, сохраняемая непосредственно в тексте, поэтому для её поддержки используется вот такой простейший интерфейс:

        template <typename PrimType>
struct Primitive
{
static tstring ToString(const PrimType&)
  { 
    throw ParseValueException("No primitive specialization");
  }
static PrimType Parse(const tstring& s) 
  { 
    throw ParseValueException("No primitive specialization");
  }
};

Для каждого примитивного типа нужно обеспечить специализацию этого шаблона. Например, вот как это сделано для типа char:

        template <> 
struct Primitive<char>
{
  static tstring ToString(constchar& f)
  { 
    tchar sz[8]; 
    return _itot(f, sz, 10); 
  }

  staticchar Parse(const tstring& s)
  { 
    __int64 n = _tstoi64(s.c_str());
    if (n < SCHAR_MIN || n > SCHAR_MAX)
      throw ParseValueException(_T("char"), s);
    returnstatic_cast<char>(n); 
  }
};

У других примитивов реализация такая же простая.

Метаданные своими руками

А где же метаданные создаются? – спросите вы. Где и когда создаются экземпляры тех объектов, о которых шла речь? Помните, мы размечали структуру Pnt? Мы создали класс-наследник Layout<Pnt> и в его конструкторе определяли разметку структуры с помощью методов Simple, Attribute, Array, Complex? Ниже приведён текст одного из этих методов:

        template <typename FieldType>
void Complex( FieldName                name,
FieldType StructType::*  offset,
              const FieldType*         pDefault,
              const Layout<FieldType>& layout = DefaultLayout<FieldType>()
            )
{ 
  push_back(new StructField<StructType, FieldType>(name,
                                                   offset,
                                                   pDefault,
                                                   layout));
}

Как видите, метод всего лишь создаёт атрибут одного поля и вставляет его в разметку. Но откуда же он берёт разметку самого поля? Она передаётся в качестве последнего параметра метода, а он, в свою очередь, имеет значение по умолчанию DefaultLayout(). Посмотрим на текст этого метода.

        template <typename DataType>
const Layout<DataType>& DefaultLayout()
{
  static DataType::LayoutDefault g_layout; // метаданные своими руками 
  return g_layout;
}

Вот где затаились сами метаданные! Они представлены как статические переменные процедуры DefaultLayout(). Применён распространённый способ: статические переменные, не инициализирующиеся до тех пор, пока не потребуются, – это намного удобнее и эффективнее открытых глобальных статических переменных. Метаклассы создаются так же, как и разметка – по требованию. Для этого есть метод DefaultMetaClass().

Важное замечание: и DefaultLayout(), и DefaultMetaClass() – это всего лишь значения по умолчанию параметров вызовов Complex и Array. Если вас не устраивает разметка по умолчанию, или же её просто нет (класс чужой), то вы можете определить свои варианты разметки и метакласса.

Вот как можно обеспечить альтернативный способ сохранения знакомого нам класса Clr:

        struct Clr_Layout2 : public Layout<Clr>
{
  Clr_Layout2()
  {
    Attribute(_T("Red"  ), &Clr::r, &Clr::black.r);
    Attribute(_T("Green"), &Clr::g, &Clr::black.g);
    Attribute(_T("Blue" ), &Clr::b, &Clr::black.b);
  }
};

const StructMetaClass<Clr>& Clr_MetaClass2()
{
  static Clr_Layout2          layout;            // метаданные - разметка. 
  static StructClassMeta<Clr> metaClass(layout); // метаданные - метакласс. return metaClass;
}

Теперь мы можем сохранять Clr ещё одним способом:

        struct LayoutDefault : public Layout<Pnt2>
{
  LayoutDefault()
  {
    ...
    Complex(_T("Clrs"), &Pnt2::colors,  _T("Clr"));// разметка по умолчанию
    Complex(_T("Clrs2"), &Pnt2::colors2, _T("Clr2"), Clr_MetaClass2());
    ...
  };
};
ПРИМЕЧАНИЕ

Попробуйте обеспечить в программе на C# два разных, параллельно работающих варианта сохранения структуры данных в xml. Думаю, такая задача заставит вас глубоко задуматься. Здесь же мы легко можем одновременно работать с одной структурой данных, сохраняя её в файлы разной структуры. Ведь мы можем обеспечить одной структуре несколько наборов метаданных!

Давайте еще раз поподробнее проследим путь (жизненный цикл) метаданных. Как только мы добавляем разметку одного поля в структуре, компилятор генерирует код процедур, генерирующих метаданные, код метаклассов, атрибутов полей и примитивов. Если при исполнении программы код сериализации не исполняется, то метаданные создаваться не будут. Но стоит один раз исполнить код сериализации, как будут проинициализированы все необходимые метаданные (проинициализируются статические переменные-разметки и переменные-метаклассы, о которых говорилось, отработают все их конструкторы, в теле которых были написаны разметку структур, заполнятся карты имён перечислений и т.д.). При последующих вызовах Xm::Load и Xm::Save эта титаническая работа производиться уже не будет – метаданные строятся один раз за время исполнения программы.

Конфигурационные файлы .Net

Ну вот, кажется, всё сложное позади. Теперь для самых терпеливых и упорных читателей, для тех, кто дочитал до этого места, у меня приготовлено «угощение»: давайте научим C++ программы читать конфигурационные файлы, принятые в среде .Net. Вспомните обычную структуру этих файлов:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="TimeoutMillisec" value="2000" /> 
  </appSettings>
<configuration>

.Net-приложение использует в своей работе раздел <configuration><appSettings>, который содержит теги add c атрибутами key и value.

Создаётся и размечается структура, соответствующая тегу add:

      namespace dotNet
{
  struct Pair
  {
    tstring key;
    tstring value;

 struct LayoutDefault : public Layout<Pair>
    {
      LayoutDefault()
      {
        Attribute(_T("key"  ), &Pair::key  );
        Attribute(_T("value"), &Pair::value);
      }
    };
  };
}

Создаётся и размечается структура, соответствующая всему конфигурационному файлу в целом (точнее его разделу appSettings):

      struct File
{
  typedef std::vector<Pair> Pairs;
  Pairs pairs;

  struct LayoutDefault : public Layout<File>
  {
    LayoutDefault()
    {
      Array(_T("appSettings" ), &File::pairs, _T("add" ));
    }
  };
};

И, наконец, нужно позаботиться об удобстве и эффективности обращения к конфигурационным данным:

      struct AppSettings
{
  typedef std::map<tstring, tstring> Values;

  AppSettings(const tstring& path)
  {
    File file;
    Xml::Load(path, _T("configuration"), &file);
    for_each(file.pairs.begin(), file.pairs.end(),
        bind1st(mem_fun1<void, AppSettings, Pair>(Add),
        this));
  }

  const tstring& GetValue(const tstring& k, const tstring& d = _T("")) const
  {
    Values::const_iterator iter = m_values.find(k);
    return iter == m_values.end() ? d : iter->second;
  }

  void Add(Pair pair)
    { m_values.insert(Values::value_type(pair.key, pair.value)); }

protected:
  Values m_values;
};
} // namespace dotNet

Единожды проделав эти простые шаги, можно повсеместно использовать этот код для чтения config-файлов:

dotNet::AppSettings appSettings(_T("app.exe.config"));
cout << appSettings.GetValue(_T("TimeoutMillisec "));

Не сложнее, чем на C#, не правда ли? Вот. Пожалуйста, угощайтесь. :)

Заключение

Кажется, идея оказалась плодотворной. Давайте подумаем, что ещё можно сделать?

  1. Во-первых, мне кажется, что неоднократно обсуждавшиеся выше ограничения структуры сериализуемых типов вполне преодолимы. Совершенно реально (если потребуется) обеспечить работу с полиморфными массивами (массивами элементов разного типа) и со структурами, содержащими указатели и ссылки. В частности, очень просто это будет сделать, если потребовать, чтобы структура данных не имела кольцевых ссылок. Класс сериализуемых типов расширится при этом очень существенно. Да, в общем, и кольцевые ссылки не очень большая проблема.
  2. Второе. Очевидно, что никогда не стоит отказываться от возможности контроля структуры xml-файла по схеме. Значит, придётся и схему составлять, и структуру данных в исходном тексте на С++ создавать, и размечать только что созданную структуру данных. Конечно, можно ограничиться только составлением схемы, а потом использовать какой либо метод автоматической генерации готовых, размеченных структур в *.cpp файлах (xslt- преобразования, например).
  3. В третьих, можно научиться сериализовать не только поля, но и свойства класса (put, get -методы).

Короче, есть ещё над чем поработать. Если у вас возникнут идеи по развитию данного метода или идеи других подходов к решению той же задачи, их всегда можно обсудить на форумах rsdn.ru. Желаю удачи!


Эта статья опубликована в журнале RSDN Magazine #5-2003. Информацию о журнале можно найти здесь
    Сообщений 39    Оценка 1785 [+0/-1]         Оценить