← Октябрь 2003 → | ||||||
1
|
3
|
4
|
5
|
|||
---|---|---|---|---|---|---|
6
|
7
|
8
|
9
|
10
|
11
|
12
|
13
|
14
|
15
|
16
|
17
|
18
|
19
|
20
|
21
|
22
|
23
|
24
|
25
|
26
|
27
|
28
|
29
|
31
|
За последние 60 дней 2 выпусков (1-2 раза в 2 месяца)
Сайт рассылки:
http://rsdn.ru
Открыта:
14-06-2000
Статистика
-5 за неделю
Программирование на Visual С++ - No.100 (C++: метаданные своими руками)
Информационный Канал Subscribe.Ru |
|
РАССЫЛКА САЙТА
RSDN.RU |
Здравствуйте, дорогие подписчики!
Как вы наверное заметили, номер у сегодняшнего выпуска довольно-таки круглый. В связи с чем команда RSDN делает вам подарок: сегодня
в рассылке ЭКСКЛЮЗИВНО публикуется статья из нового номера RSDN Magazine (#5'03), который должен в скором времени появиться в продаже! А также хочется напомнить, что уже сейчас можно купить RSDN Magazine #4'03 со следующими материалами:
На сопроводительном CD-ROM к RSDN Magazine #4 вы найдете:
C++: метаданные своими руками Автор: Андрей МартыновПроблемаПрежде всего, надо пояснить подзаголовок. «Классический С++» здесь означает «не MC++», то есть не C++ with managed extensions. Это важное уточнение, так как работа с XML в среде .Net – это лёгкое и приятное занятие. В этой среде имеются встроенные средства сериализации типов, основанные на метаданных. Но что делать, если вы пишете на старом («классическом») С++, а читать/писать xml-файлы вам нужно не в меньшей степени, чем коллегам, пишущим на .Net-совместимых языках? Вот этой цели – скрасить жизнь С++ программистов, работающих с XML, – и посвящена библиотека классов (шаблонов), которая будет представлена ниже. Второе уточнение подзаголовка статьи – что означает выражение «простые Xml-файлы»? Здесь «простота» подразумевает, что до полного повторения всех возможностей, имеющихся в .Net, дело не дошло (пока), решена лишь задача, описание которой приведено ниже. Имеется структура данных, содержащая подструктуры любой степени вложенности, а также массивы. И поля структур, и элементы массивов имеют типы-значения, т.е. среди них нет ни указателей, ни ссылок. Задача состоит в том, чтобы обеспечить универсальный алгоритм чтения/записи такой структуры в виде xml-файла. Изменение структуры данных не должно приводить к перепрограммированию алгоритма чтения. Чтение должно быть основано на неких данных, которыми предварительно снабжены (размечены) все типы, участвующие в считываемой/записываемой структуре. Короче, при таком подходе программист не должен для каждого типа сохраняемых данных писать процедуру обхода XMLDOM-дерева. Он лишь декларирует (описывает, размечает) структуру данных специальным образом, чтобы потом использовать универсальный алгоритм чтения/записи. Ровно то, что называется декларативным подходом в программировании.
РешениеПредлагаю для начала, не вдаваясь в подробности устройства библиотеки, посмотреть на то, как она применяется, и что это даёт. Начнём с простейшего. Правило первое – чтобы записать структуру в 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; const static Clr white; const static 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::whitel; 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 : public EnumMeta<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; const static bool defaultHoliday = true; const static 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> Вот, собственно, и все главные возможности библиотеки. Если они вас не заинтересовали, то на этом чтение статьи можно закончить. Если же сказанное выше представляет для вас интерес, то я продолжу. ПодробностиВ принципе, можно и не вникать в устройство библиотеки. Приведённых примеров достаточно, чтобы по аналогии с ними решать простейшие задачи. Однако лучше всё же заглянуть «под капот». Это пригодится, если будет сделано что-то не так, и компилятор выдаст какое-то непонятное диагностическое сообщение. Стоит предупредить, при работе с библиотекой шаблонов расшифровка сообщений компилятора бывает порой делом весьма сложным. Знание деталей тут совсем не помешает. А может быть, кто-то захочет усовершенствовать библиотеку, дополнить её новыми возможностями. Надеюсь, что в этом случае сказанное ниже поможет вам лучше сориентироваться.
«Деревянное» хранилищеВ самом начале разработки библиотеки было принято решение не ограничиваться работой только с XML-файлами. Ведь на свете есть много иерархически устроенных хранилищ данных, и работу с ними можно организовать единообразно. Ниже приведён интерфейс, который был принят для описания хранилища данных в виде дерева, «деревянного» хранилища: interface INamedNodeList { virtual ~INamedNodeList(){} virtual INode* GetByName(const tstring& name) const = 0; }; interface INodeArray { virtual ~INodeArray(){} virtual long Count() const = 0; virtual INode* GetByIndex(long index) const = 0; }; interface INode // Абстрактная ветка (элемент или атрибут) { virtual ~INode(){} virtual tstring ReadText() const = 0; virtual void 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): К сожалению, реестр не поддерживает массивы узлов (ключей), поэтому приходится эмулировать массивы при помощи специального вида имён узлов.
МетаклассыКак уже упоминалось, данный вариант библиотеки накладывает определённые ограничения на структуру данных, предназначенных для чтения/записи в xml-файл. Принята следующая модель данных: данные могут быть или примитивными типами, или структурами (наборами полей разных типов), или массивами (последовательностями однотипных элементов). Поля и элементы массивов могут быть данными примитивных типов или же структурами и массивами. Короче, данные – это примитивы, структуры и массивы, состоящие из примитивов, структур и массивов, которые в свою очередь состоят из примитивов, структур и массивов и т.д. Теперь вам будет легко понять, что такое метакласс. Метакласс – это тип класса (класс класса, тип типа), то есть метакласс – это сущность, которая показывает, к какому семейству структур данных принадлежит данная структура. Это примитив? Это структура (в узком смысле)? Или это массив? Попробуем выразить эту мысль на языке C++. Метакласс превращается в шаблонный тип (параметр шаблона – сам класс), реализующий следующий интерфейс. template <typename Data> struct MetaClass { virtual ~MetaClass() = 0; virtual Data ReadNode (const INode&) const = 0; virtual void 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 { virtual void ReadField (const INode&, StructType*) = 0; virtual void 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(const char& f) { tchar sz[8]; return _itot(f, sz, 10); } static char Parse(const tstring& s) { __int64 n = _tstoi64(s.c_str()); if (n < SCHAR_MIN || n > SCHAR_MAX) throw ParseValueException(_T("char"), s); return static_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()); ... }; };
Давайте еще раз поподробнее проследим путь (жизненный цикл) метаданных. Как только мы добавляем разметку одного поля в структуре, компилятор генерирует код процедур, генерирующих метаданные, код метаклассов, атрибутов полей и примитивов. Если при исполнении программы код сериализации не исполняется, то метаданные создаваться не будут. Но стоит один раз исполнить код сериализации, как будут проинициализированы все необходимые метаданные (проинициализируются статические переменные-разметки и переменные-метаклассы, о которых говорилось, отработают все их конструкторы, в теле которых были написаны разметку структур, заполнятся карты имён перечислений и т.д.). При последующих вызовах 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#, не правда ли? Вот. Пожалуйста, угощайтесь. :) ЗаключениеКажется, идея оказалась плодотворной. Давайте подумаем, что ещё можно сделать?
Короче, есть ещё над чем поработать. Если у вас возникнут идеи по развитию данного метода или идеи других подходов к решению той же задачи, их всегда можно обсудить на форумах rsdn.ru. Желаю удачи! |
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||