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

Использование метаданных в программах на языке C++

Автор: Владислав Юдин
Источник: RSDN Magazine #1-2005
Опубликовано: 22.05.2005
Исправлено: 02.09.2005
Версия текста: 1.0
Введение
Структурные метаданные
Использование атрибутивной информации
Возможный вариант реализации
Пример использования атрибутивной информации
Использование метаклассов
Обработка элементов метакласса
Заключение
Литература

Самое сложное – понять простые вещи.
Народная мудрость

Примеры к статье

Введение

Метаданные – это данные о данных. Применительно к С++ [1], метаданные – это дополнительная информация о типах, которая позволяет выполнять различные операции над ними стандартным способом.

В настоящее время большое количество библиотек используют метаданные для различных целей. Например:

Особенностью существующих решений является то, что они используют реализации метаданных, адаптированные для обеспечения основной функциональности.

Цель данной статьи – показать, что метаданные – это мощный механизм, требующий выделения в отдельный компонент, который хотелось бы видеть в стандартной библиотеке С++.

Это утверждение основывается на опыте автора, полученном при разработке системы управления нейтронографическими спектрометрами в Лаборатории нейтронной физики имени И.М. Франка Объединенного института ядерных исследований (Дубна, Россия) [9]. В данном проекте метаданные использовались для сериализации/десериализации данных, межпроцессного взаимодействия и взаимодействия между языками Python и С++.

Надо отметить, что приведенные ниже примеры кода показывают не законченное решение, а лишь обозначают возможные подходы к решению поставленной задачи. Все примеры компилировались при помощи Microsoft Visual C++ 2003 (версия 7.1) [10].

Структурные метаданные

Рассмотрим простой случай. Пусть необходимо передать по сети следующую структуру:

struct SSimple
{
  int  a;
  char  b;
  double  c;
};

Чаще всего код будет иметь вид:

SSimple simple;
// инициализация
obj.SendData( &simple, sizeof(simple) );

Этот тривиальный код будет работать некорректно в случае разного выравнивания данных на стороне клиента и на стороне сервера. Классическое решение данной проблемы – управление выравнивание с помощью директив компилятора (в данном случае #pragma pack).

Усложним задачу: пусть взаимодействуют компьютеры с процессорами Intel[11] и Motorola[12]. В этом случае придется поменять порядок байтов.

Прямолинейное решение будет иметь примерно такой вид:

obj.Pack( SwapInt(simple.a), sizeof(int));
obj.Pack( simple.b, sizeof (char));
obj.Pack( SwapDouble(simple.c), sizeof (double));
obj.SendBuffer();

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

Попробуем создать вариант решения, лишенный перечисленных недостатков. Вводим описание структуры данных:

template <class H> void Metadata(H& h, SSimple& data)
{
  h(data.a);
  h(data.b);
  h(data.c);
}

Обработчик, используемый для упаковки данных в буфер:

class SPackerException{};
class SPacker
{
// max_buffer_size – максимальный размер пакета данных
  unsigned char  m_buffer[max_buffer_size]; 
  size_t    m_position;
  void pack(const void* data, size_t size)
  {
    if (m_position+size > max_buffer_size) throw SPackerException();
    memmove (m_buffer + m_position, data, size);
    m_position+=size;
  }

template <class T> void SimplePack(T t) 
{
  pack(&t, sizeof(T));
}

public:

  SPacker() : m_position(0){}

  const unsigned char* Buffer() const {return m_buffer;}

  size_t Size() const {return m_position;}

template <class T> void operator()(T& data) { Metadata(*this, data); }

  void operator()(char data) { SimplePack(data); }

  void operator()(int data) { SimplePack(Swap<sizeof(int)>(data)); }

  void operator()(double data) { SimplePack(Swap<sizeof(double)>(data)); }

};

Тогда посылка запросов будет выглядеть:

SPacker packer;
packer(simple);
obj.SendData(packer.Buffer(), packer.Size());

Обработчик для распаковки строится аналогичным образом (только операторы вызова функций (скобки) получают параметры по ссылке).

Недостаток данного решения – переменная simple не модифицируется, но передать константную ссылку на нее нельзя.

Возможное решение данной проблемы. Добавим два класса:

struct SConstSerialize
{
  template <class T> struct Requred
  {
    typedef const T& Type;  
  };
};

struct SNotConstSerialize
{
  template <class T> struct Requred
  {
    typedef T& Type;  
  };
};

Если обработчик модифицирует данные, то он наследуется от SNotConstSerialize, а если нет, то от SConstSerialize.

class SPacker: public SConstSerialize
{
...
// без изменений
  template <class T> void operator()(const T& data) { Metadata(*this, data); }
...
// без изменений
};

Тогда функция, описывающая метаданные, будет выглядеть:

template <class H> void Metadata(
  H& h, typename H::Requred< SSimple >::Type & data)
{
  ... // без изменений 
}

Если требуется поддержка контейнеров (например, std::vector), нужно добавить соответствующий оператор вызова функции:

template <class T> void operator()(const std::vector<T>& data)
{
  (*this)((int)data.size()); // возможно, преобразование некорректное
  for (std::vector<T>::const_iterator i = data.begin(), 
    i_end = data.end(); i != i_end; ++i)
    (*this)(*i);
}

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

У такого решения есть следующие достоинства:

Причем применение подобного подхода дает преимущества даже на относительно простых задачах.

Использование атрибутивной информации

Для проведения более сложных преобразований, требуется дополнительная информация об элементах структуры, которую будем называть атрибутивной.

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

На реализацию атрибутов были наложены следующие требования:

Возможный вариант реализации

Пусть атрибут имеет следующую структуру:

// Имя типа атрибута начинается с буква А
struct A[имя атрибута]
{
  typedef  [тип атрибута]  value_type;
  value_type      value;
  A[имя атрибута] (value_type v) : value(v){}
// если для атрибута существует значение по умолчанию, то добавляем функцию
  inline static value_type Default(){return <значение по умолчанию>;}
};

С каждым элементом ассоциируется контейнер атрибутов:

SAttributeCnt[количество атрибутов]

Контейнер, не содержащий атрибутов, позволяет только получать значения атрибутов по умолчанию. Типы атрибутов, передаются как параметры шаблона. Контейнер наследует требуемые типы атрибутов. В классе контейнера описывается шаблонная структура ATraits, в которой объявляется значение flag. Для произвольного значения шаблонного типа оно имеет значение false. Для типов атрибутов, содержащихся в контейнере, задается специализация данного шаблона, в котором значение flag равно true.

На основании этого флага происходит обращение либо к значению атрибута (true), либо к значению по умолчанию (если оно существует). За это отвечает шаблонная структура SGet:

namespace Details
{
  template <bool F = false> struct SGet
  {
    template <class T, class F> 
      static inline typename T::value_type Do(const F& f)
    {
      return T::Default();
    }
  };
  
  template <> struct SGet<true>
  {
    template <class T, class F> 
      static inline typename T::value_type Do(const F& f)
    {
      return f.T::value;
    }
  };
}

Здесь:

Контейнеры имеют следующий вид:

struct SAttributeCnt0
{
  SAttributeCnt0(){}
  template <class C> struct ATraits
  {
    enum { flag = false };
  };

  template <class T> typename T::value_type Get(void) const
  {
    return SGet<ATraits<T>::flag>::Do<T>(*this);
  }
};

template <class A1> struct SAttributeCnt1: public A1
{
  SAttributeCnt1(typename A1::value_type _a1)
:  A1(_a1) {}
  template <class C> struct ATraits { enum {flag = false }; };
  template <> struct ATraits<A1> { enum {flag = true}; };

  template <class T> typename T::value_type Get(void) const 
  {
    return SGet<ATraits<T>::flag>::Do<T>(*this);
  }
};

Обращение к атрибуту происходит следующим образом:

template <class AttributeCnt […]> 
  return_type SomeFunc(const AttributeCnt& attributeCnt [...])
{
  attributes.Get< [тип атрибута] >();
}

Для автоматического определения типа контейнера определим шаблонную структуру SAttributeCnt:

namespace Details
{
  struct svoid{};
}

template <class A1 = Details::svoid, 
  class A2 = Details::svoid, 
  class A3 = Details::svoid>
struct SAttributeCnt
{
  typedef SAttributeCnt3<A1, A2, A3> Type;
};

template <class A1, class A2> struct SAttributeCnt<A1,A2>
{
  typedef SAttributeCnt2<A1, A2> Type;
};

template <class A1> struct SAttributeCnt<A1>  
{
  typedef SAttributeCnt1<A1> Type;
};

template <> struct SAttributeCnt<>
{
  typedef SAttributeCnt0 Type;
};

Данная реализация может быть расширена для поддержки требуемого количества атрибутов. Тогда объявление контейнера будет иметь следующий вид:

SAttributeCnt < [тип атрибута 1],  [тип атрибута 2], …>::Type attributeCnt( [значение атрибута 1], [значение атрибута 2], …);

Пример использования атрибутивной информации

Пусть задана следующая структура:

struct SPoint
{
  int x;
  int y;
};

Пусть для сохранения данных в XML-формате[13] требуется применение следующих атрибутов:

1. Атрибут, задающий имя элемента:

struct AName 
{
  typedef const char* value_type;
  value_type      value;
  AName(value_type v): value(v){}
};

2. Атрибут, говорящий, нужно ли сохранять элемент как XML-атрибут:

struct AXMLAttribute 
{
  typedef bool  value_type;
  value_type    value;
  AXMLAttribute(value_type v) : value(v){ }
  inline static value_type Default(){ return false; }
};

3. Атрибут, задающий название элементов массива:

struct AXMLEntryName
{
  typedef const char* value_type;
  value_type  value;
  AXMLEntryName(value_type v) : value(v){ }
  inline static value_type Default(){ return "entry"; }
};

Тогда описание и использование атрибутивной информации будет иметь вид:

template <class H> 
  void Metadata(H& h, typename H::Requred< SPoint >::Type data)
{
  static AttrCnt<AName, AXMLAttribute>::Type a_x("x",true);
  h(data.x, a_x);
  static AttrCnt<AName>::Type a_y("y");
  h(data.y, a_y);
}

class SSameHandler: public SConstSerialize
{
...
  template <class A> void operator()(int data, const A& a)
  {
    const char* name = a.Get<AName>();
    bool fXMLAttribute = a.Get<AXMLAttribute>();
    ...
  }
};

Данный подход имеет недостатки:

Использование метаклассов

Что бы устранить данные недостатки, откажемся от использования функции для задания метаданных и используем для этих целей метакласс:

template <class T> struct Meta;

Здесь T – тип класса (структуры), для которого задаются метаданные.

Возможный вариант реализации метакласса будет содержать:

1. Тип описываемого класса:

typedef T value_type; 

2. Вложенная структура Entry, которая будет специализироваться для каждого элемента описываемого класса:

  template <int Index> struct Entry;

3. Количество элементов класса:

  enum { entries = N };

4. Тип контейнера атрибутов, сопоставленного с классом в целом:

typedef SAttributeCnt<…>::Type TAttributes;

5. Функция, возвращающая ссылку на этот контейнер атрибутов:

static const TAttributes& Attributes()
{
  static const TAttributes a(...);
  return a;
}

6. Пустая специализация структуры Entry для значения entries, нужная для автоматического контроля значения entries:

template <> struct Entry<entries>{ };

7. Специализации структуры Entry, для каждого элемента класса:

template <> struct Entry<index>

Специализация структуры Entry содержит следующие элементы:

1. Тип элемента:

typedef E type;

2. Тип контейнера атрибутов:

typedef SAttributeCnt<…>::Type TAttributes;

3. Функция, возвращающая ссылку на контейнер атрибутов:

static const TAttributes& Attributes()
{
  static const TAttributes a(...);
  return a;
}

4. Функции, возвращающие константную и не константную ссылки на элемент:

inline static type& Value(value_type& data){ return data.[name];}
inline static const type& Value(const value_type& data){ return data.[name];}

Тогда описание метакласса для структуры SPoint будет выглядеть так:

template <> struct Meta<SPoint>
{
  typedef SPoint value_type;
  template <int Index> struct Entry;

  enum {entries = 2};
  template <> struct Entry<entries>{};

  typedef SAttributeCnt<AVersion>::Type TAttributes;
  static const TAttributes& Attributes()
  {
    static const TAttributes a(10);
    return a;
  }

  template <> struct Entry<0>
  {
    typedef SAttributeCnt<AName, AXMLAttribute>::Type TAttributes;
    static const TAttributes& Attributes()
    {
      static const TAttributes a("x",true);
      return a;
    }

    typedef int type;
    inline static type& Value( value_type& data){ return data.x;}
    inline static const type& Value( const value_type& data){ return data.x;}
  };

  template <> struct Entry<1>
  //... аналогично template <> struct Entry<0>

}

Здесь атрибут AVersion описывает версию структуры:

struct AVersion 
{
  typedef unsigned int  value_type;
  value_type      value;
  AVersion(value_type v) : value(v){}
  inline static value_type Default(){return 0;}
};

Для упрощения задания метакласса введем макросы:

1. Задает тип и количество элементов в классе:

#define META(type, items) template <> struct Meta< type > {\
  typedef type value_type;      \
  template <int Index> struct Entry;  \
  enum {entries = items};        \
  template <> struct Entry<entries>{};

2. Задает конец описания метакласса:

#define META_END };

3. Задает пустой контейнер атрибутов:

#define ATTR_EMPTY typedef SAttributeCnt<>::Type TAttributes;\
  static inline const TAttributes& Attributes() { \
  static const TAttributesa; return a; }

4. Задает начало описания контейнера атрибутов (после него идет список типов атрибутов):

#define ATTR_TYPES typedef SAttributeCnt<

5. Задает начало описания значений атрибутов:

#define ATTR_VALUES >::Type TAttributes; \
  static inline const TAttributes& Attributes() { \
    static const TAttributes a( 

6. Завершает описание контейнера атрибутов:

#define ATTR_END ); return a; }

7. Начинает описание элемента:

#define META_ENTRY(index) template <> struct Entry<index> {

8. Задает тип и имя элемента:

#define META_ENTRY_INFO(type_name, name) \
  typedef type_name type;      \
  inline static type& Value(value_type& data){ return data.name;}  \
  inline static const type& Value(const value_type& data){\
  return data.name; } \
};

9. Задает тип и имя элемента, тип которого массив.

#define META_ENTRY_ARR_INFO(type_name, size, name) \
    typedef type_name type size;      \
    inline static type& Value( value_type& data){ return data.name;}  \
    inline static const type& Value( const value_type& data){ return data.name;} \
  };

С применение данных макросов задание метакласса для структуры SPoint примет следующий вид:

META(SPoint, 2)
  ATTR_TYPES AVersion ATTR_VALUES 3 ATTR_END
  META_ENTRY(0)
    ATTR_TYPES AName, AXMLAttribute ATTR_VALUES "x", true ATTR_END
    META_ENTRY_INFO(int, x)
  META_ENTRY(1)
    ATTR_TYPES AName ATTR_VALUES "y" ATTR_END
    META_ENTRY_INFO(int, y)
META_END

Обработка элементов метакласса

Структура SProcessStruct выполняет цикл этапа компиляции [14] с вызовом функции ProcessItemStruct (обработать элемент структуры) обработчика метаданных, где SprocessStructCondition задает условие остановки цикла:

template <class T, int index> struct SProcessStructCondition
{
  enum { value = (index >= Meta<T>::entries)};
};

SProcessStructCondition::value будет иметь значение true, если элемента с индексом index для класса T не существует.

Шаблон SProcessStruct, содержащий значения по умолчанию для параметров шаблона и точку выхода из цикла:

template <class T, int index = 0, bool stop = SProcessStructCondition<T, index>::value> struct SProcessStruct
{
  template <class H> inline static void Do(
    H& h, typename H::Requred<T>::Type data){}
};

Специализация шаблона SProcessStruct, содержащая тело цикла:

template <class T, int index> struct SProcessStruct<T, index, false>
{
  template <class H> static void Do(H& h, typename H::Requred<T>::Type data)
  {
    h.ProcessItemStruct< Meta<T>::Entry<index> >(
      Meta<T>::Entry<index>::Value(data));
    SProcessStruct<T, index+1>::Do(h, data);
  }
};

Тогда для вызова функции обработчика ProcessItemStruct для каждого элемента класса будет использоваться следующая конструкция:

template <class T> void SomeFunc([const] T& data)
{
  SSomeHandler handler(…);
SProcessStruct<T>::Do(handler, data);
}

Функция ProcessItemStruct будет иметь прототип:

template <class E, class T> void ProcessItemStruct([const] T& data)

, где E – тип, сопоставленный с элементом структуры, Т – тип элемента структуры.

Рассмотрим пример применения описанного выше. Пусть требуется производить запись данных в поток в XML-формате. Для этого можно создать обработчик CXMLWriter. В конструктор передается ссылка на открытый текстовый поток. Имеются шаблонные функции для обработки трех целевых категорий типов:

1. Простые типы (PrintLeaf). Для них должна быть допустима операция вывода в поток (operator <<).

Параметры:

2. Структуры (ProcessStruct). Для них должны быть описаны метаклассы.

Параметры:

3. Контейнеры (ProcessArray). Они должны поддерживать итераторы ввода.

Параметры:

ProcessStruct вызывает функцию ProcessItemStruct (через вызов SProcessStruct<T>::Do(*this, data); ) для каждого элемента структуры.

ProcessArray вызывает функцию ProcessItemCnt для каждого элемента контейнера, ее прототип:

template<class T> void ProcessItemCnt(const T& data, const char* name);

, где:

class CXMLWriter : public SConstSerialize
{
  // Implementation
public:  
  typedef std::ostream stream_type;

  CXMLWriter(CXMLWriter::stream_type& stream);

  template <class T> void PrintLeaf(const T& data, bool fXMLAttribute, 
    const char * name);

  template <class T> void ProcessStruct(const T& data, const char* name);

  template <class T> void ProcessArray(const T begin, const T end, 
    const char* name, const char* entry_name);

  ProcessItemCnt
  ProcessItemCnt
  ...
  ProcessItemStruct
  ProcessItemStruct
  ...
};

При прямолинейном подходе нужно было бы определить, например, для ProcessItemStruct:

template <class E, class T> void ProcessItemStruct(const T& data);

структура элемент структуры;

  template <class E, class T, size_t N> void ProcessItemStruct(const T (&data)[N]);

массив элемент структуры;

  template <class E, class T> void ProcessItemStruct(const std::vector<T>& data);

std::vector элемент структуры;

  template <class E, class K, class V> void ProcessItemStruct(const std::map<K,V>& data);

std::map элемент структуры;

  template <class E> void ProcessItemStruct(const std::string& data);

std::string элемент структуры;

template <class E> void ProcessItemStruct(int data);

int элемент структуры;

… - и т. д.

А также аналогичные функции ProcessItemCnt.

Фактически, для каждого типа данных надо добавить две функции (ProcessItemStruct и ProcessItemCnt), которые адаптировали бы эти типы и дополнительные входные данные для вызова соответствующих функций преобразования (PrintLeaf, ProcessStruct и ProcessArray).

Воспользуемся тем, что эти адаптирующие функции обладают определенной общностью (например, тела функций ProcessItemStruct для std::map и std::vector будут идентичными).

Для этого введем правила преобразования из типов С++ в типы, поддерживаемые CXMLWriter.

Типы правил преобразования (их количество не всегда будет совпадать с целевыми типами, например, можно ввести правило, согласно которому проход по элементам контейнера будет выполняться в обратном порядке):

  struct RSimple{};
  struct RStruct{};
  struct RCnt{};

Шаблонная структура RuleDescriptor связывает тип С++ и правило преобразования, которое к нему необходимо применить (запрос имеет вид RuleDescriptor<SomeType>::Rule):

template <class T>    struct RuleDescriptor  { typedef RStruct Rule; };
template <>      struct RuleDescriptor<int>  { typedef RSimple Rule; };
template <>      struct RuleDescriptor<char> { typedef RSimple Rule; };
...
template <>      struct RuleDescriptor<std::string> { typedef RSimple Rule;};
//template <>    struct RuleDescriptor<std::string> { typedef RCnt Rule; };

template <class T, size_t N>
struct RuleDescriptor<T[N]>
{
  typedef RCnt Rule;
};

template <class T> 
struct RuleDescriptor<std::vector<T> >
{
  typedef RCnt Rule;
};

template <class K, class V>  
struct RuleDescriptor<std::map<K,V> >
{
  typedef RCnt Rule;
};
...
ПРИМЕЧАНИЕ

Обратите внимание, что для std::string можно применить разные правила преобразования.

Объявим шаблонную структуру Item.

template <class Rule> struct Item;

Ее специализации будут содержать шаблонные функции:

namespace CXMLWriterHelper
{
// struct
  template <class Rule> struct Item;

  template <> struct Item<RStruct>
  {
    template <class E, class T> 
    static void Struct(CXMLWriter& h, const T& data)
    {
      h.ProcessStruct(data, E::Attributes().Get<AName>());
    }

    template <class T> 
    static void Cnt(CXMLWriter& h, const T& data, const char* name)
    {
      h.ProcessStruct(data, name);
    } 
  };

  template <> struct Item<RSimple>
  {
    template <class E, class T> 
    static void Struct(CXMLWriter& h, const T& data)
    {
      h.PrintLeaf(data, 
        E::Attributes().Get<AXMLAttribute>(), E::Attributes().Get<AName>());
    } 

    template <class T> 
    static void Cnt(CXMLWriter& h, const T& data, const char* name)
    {
      h.PrintLeaf(data, false, name);
    } 
  };

  template <> struct Item<RCnt>
  {
    template <class E, class T> 
    static void Struct(CXMLWriter& h, const T& data)
    {
      h.ProcessArray(data.begin(), data.end(), 
        E::Attributes().Get<AName>(), E::Attributes().Get<AXMLEntryName>());
    } 

    template <class T> 
    static void Cnt(CXMLWriter& h, const T& data, const char* name)
    {
      h.ProcessArray(data.begin(), 
        data.end(), name, AXMLEntryName::Default());
    } 

    template <class E, class T, size_t N> 
    static void Struct(CXMLWriter& h, const T (&data)[N])
    {
      h.ProcessArray(data, data + N, 
        E::Attributes().Get<AName>(), E::Attributes().Get<AXMLEntryName>());
    } 

    template <class T, size_t N> 
    static void Cnt(CXMLWriter& h, const T (&data)[N], const char* name)
    {
      h.ProcessArray(data, data + N, name, AXMLEntryName::Default());
    }
  };
}

Теперь функции ProcessItemStruct и ProcessItemCnt можно переписать в обобщенном виде:

template <class E, class T> void ProcessItemStruct(const T& data)
{
  CXMLWriterHelper::Item<CXMLWriterHelper::RuleDescriptor<T>::Rule>
     ::Struct<E>(*this, data);
}
template<class T> void ProcessItemCnt(const T& data, const char* name)
{
  CXMLWriterHelper::Item<CXMLWriterHelper::RuleDescriptor<T>::Rule>::Cnt(
    *this, data, name);
}

Теперь для добавления возможности обработки нового типа данных достаточно добавить специализацию шаблона RuleDescriptor. Также можно добавить и новое правило преобразования. Сделать это можно в любом месте программы, без модификации класса CXMLWriter.

Построение обработчика для чтения данных аналогично, вы можете убедиться в этом на примере обработчика, выполняющего чтение данных из реестра Windows [15] (см. registry_rw.h).

Заключение

Фактически введение метаданных позволяет производить управляемое преобразование типов (managed_cast).

В статье не описано применение метаданных для перечислений (enum), методов классов, полиморфных классов и т.д. Также за рамками статьи остались вопросы, связанные с представлением и использованием метаданных на этапе выполнения (аналог метаданных .NET). В полноценной библиотеке поддержки метаданных все это должно присутствовать.

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

Литература

  1. Б. Страуструп, Язык программирования С++, спец. изд./Пер. с англ. – М.; СПб.: «Издательство БИНОМ» - «Невский Диалект», 2001 г. – 1099 стр.
  2. MFC Reference
  3. Windows Template Library (WTL)
  4. Dialog Data Exchange
  5. Dialog Data Validation
  6. Record Field Exchange (RFX)
  7. Boost Library
  8. Python
  9. Frank Laboratory of Neutron Physics
  10. Microsoft Visual C++ Developer Center
  11. Intel Corporation
  12. Motorola
  13. Extensible Markup Language (XML)
  14. Дэвид Вандевурд, Николаи М. Джосаттис. Шаблоны С++. Справочник разработчика. "Вильямс", 2003
  15. Дж. Рихтер, Дж. Кларк Программирование серверных приложений для Microsoft Winodws 2000. Мастер-класс. /Пер. с англ. – СПб.: Питер; М.: Издательско-торговый дом «Русская редакция», 2001. – 592 стр.

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