Опубликован: 10.10.2006 | Уровень: специалист | Доступ: свободно
Лекция 13:

Проектирование библиотек

< Лекция 12 || Лекция 13: 1234567891011

13.5 Динамическая информация о типе

Иногда бывает полезно знать истинный тип объекта до его использования в каких-либо операциях. Рассмотрим функцию my(set&) из \S 13.3.

void my_set(set& s)
{
   for ( T* p = s.first(); p; p = s.next()) {
       // мой код
   }
   // ...
}

Она хороша в общем случае, но представим,- стало известно, что многие параметры множества представляют собой объекты типа slist. Возможно также стал известен алгоритм перебора элементов, который значительно эффективнее для списков, чем для произвольных множеств. В результате эксперимента удалось выяснить, что именно этот перебор является узким местом в системе. Тогда, конечно, имеет смысл учесть в программе отдельно вариант с slist. Допустив возможность определения истинного типа параметра, задающего множество, функцию my (set&) можно записать так:

void my(set& s)
{
   if (ref_type_info(s) == static_type_info(slist_set)) {
      // сравнение двух представлений типа

      // s типа slist

      slist& sl = (slist&)s;
      for (T* p = sl.first(); p; p = sl.next()) {

         // эффективный вариант в расчете на list

      }
 }
 else {

    for ( T* p = s.first(); p; p = s.next()) {

         // обычный вариант для произвольного множества

     }
 }
 // ...
          }

Как только стал известен конкретный тип slist, стали доступны определенные операции со списками, и даже стала возможна реализация основных операций подстановкой.

Приведенный вариант функции действует отлично, поскольку slist - это конкретный класс, и действительно имеет смысл отдельно разбирать вариант, когда параметр является slist_set. Рассмотрим теперь такую ситуацию, когда желательно отдельно разбирать вариант как для класса, так и для всех его производных классов. Допустим, мы имеем класс dialog_box из \S 13.4 и хотим узнать, является ли он классом dbox_w_str. Поскольку может существовать много производных классов от dbox_w_str, простую проверку на совпадение с ним нельзя считать хорошим решением. Действительно, производные классы могут представлять самые разные варианты запроса строки. Например, один производный от dbox_w_str класс может предлагать пользователю варианты строк на выбор, другой может обеспечить поиск в каталоге и т.д. Значит, нужно проверять и на совпадение со всеми производными от dbox_w_str классами. Это так же типично для узловых классов, как проверка на вполне определенный тип типична для абстрактных классов, реализуемых конкретными типами.

void f(dialog_box& db)
{
   dbox_w_str* dbws = ptr_cast(dbox_w_str, &db);
   if (dbws) {  // dbox_w_str
      // здесь можно использовать dbox_w_str::get_string()
   }
   else {

     // ``обычный'' dialog_box
   }

   // ...
 }

Здесь "операция" приведения ptr_cast() свой второй параметр (указатель) приводит к своему первому параметру (типу) при условии, что указатель настроен на объект, тип которого совпадает с заданным (или является производным классом от заданного типа). Для проверки типа dialog_box используется указатель, чтобы после приведения его можно было сравнить с нулем.

Возможно альтернативное решение с помощью ссылки на dialog_box:

void g(dialog_box& db)
{
  try {
      dbox_w_str& dbws = ref_cast(dialog_box,db);

      // здесь можно использовать dbox_w_str::get_string()

   }
   catch (Bad_cast) {

      // ``обычный'' dialog_box

   }

   // ...
}

Поскольку нет приемлемого представления нулевой ссылки, с которой можно сравнивать, используется особая ситуация, обозначающая ошибку приведения (т.е. случай, когда тип не есть dbox_w_str ). Иногда лучше избегать сравнения с результатом приведения.

Различие функций ref_cast() и ptr_cast() служит хорошей иллюстрацией различий между ссылками и указателями: ссылка обязательно ссылается на объект, тогда как указатель может и не ссылаться, поэтому для указателя часто нужна проверка.

13.5.1 Информация о типе

В С++ нет иного стандартного средства получения динамической информации о типе, кроме вызовов виртуальных функций.

Хотя было сделано несколько предложений по расширению С++ в этом направлении.

Смоделировать такое средство довольно просто и в большинстве больших библиотек есть возможности динамических запросов о типе. Здесь предлагается решение, обладающее тем полезным свойством, что объем информации о типе можно произвольно расширять. Его можно реализовать с помощью вызовов виртуальных функций, и оно может входить в расширенные реализации С++.

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

typeid static_type_info(type) // получить typeid для имени типа
typeid ptr_type_info(pointer) // получить typeid для указателя
typeid ref_type_info(reference) // получить typeid для ссылки
pointer ptr_cast(type,pointer)  // преобразование указателя
reference ref_cast(type,reference)  // преобразование ссылки

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

Большинство пользователей, которым вообще нужна динамическая идентификация типа, может ограничиться операциями приведения ptr_cast() и ref_cast(). Таким образом пользователь отстраняется от дальнейших сложностей, связанных с динамической идентификацией типа. Кроме того, ограниченное использование динамической информации о типе меньше всего чревато ошибками.

Если недостаточно знать, что операция приведения прошла успешно, а нужен истинный тип (например, объектно-ориентированный ввод-вывод), то можно использовать операции динамических запросов о типе: static_type_info(), ptr_type_info() и ref_type_info(). Эти операции возвращают объект класса typeid. Как было показано в примере с set и slist_set, объекты класса typeid можно сравнивать. Для большинства задач этих сведений о классе typeid достаточно. Но для задач, которым нужна более полная информация о типе, в классе typeid есть функция get_type_info():

class typeid {
    friend class Type_info;
private:
    const Type_info* id;
public:
    typeid(const Type_info* p) : id(p) { }
    const Type_info* get_type_info() const { return id; }
    int operator==(typeid i) const ;
};

Функция get_type_info() возвращает указатель на неменяющийся (const) объект класса Type_info из typeid. Существенно, что объект не меняется: это должно гарантировать, что динамическая информация о типе отражает статические типы исходной программы. Плохо, если при выполнении программы некоторый тип может изменяться.

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

13.5.2 Класс Type_info

В классе Type_info есть минимальный объем информации для реализации операции ptr_cast() ; его можно определить следующим образом:

class Type_info {
    const char* n;       // имя
    const Type_info** b; // список базовых классов
public:
    Type_info(const char* name, const Type_info* base[]);

    const char* name() const;
    Base_iterator bases(int direct=0) const;
    int same(const Type_info* p) const;
    int has_base(const Type_info*, int direct=0) const;
    int can_cast(const Type_info* p) const;

    static const Type_info info_obj;
    virtual typeid get_info() const;
    static typeid info();
};

Две последние функции должны быть определены в каждом производном от Type_info классе.

Пользователь не должен заботиться о структуре объекта Type_info, и она приведена здесь только для полноты изложения. Строка, содержащая имя типа, введена для того, чтобы дать возможность поиска информации в таблицах имен, например, в таблице отладчика. С помощью нее а также информации из объекта Type_info можно выдавать более осмысленные диагностические сообщения. Кроме того, если возникнет потребность иметь несколько объектов типа Type_info, то имя может служить уникальным ключом этих объектов.

const char* Type_info::name() const
{
  return n;
}

int Type_info::same(const Type_info* p) const
{
  return this==p || strcmp(n,p->n)==0;
}

int Type_info::can_cast(const Type_info* p) const
{
  return same(p) || p->has_base(this);
}

Доступ к информации о базовых классах обеспечивается функциями bases() и has_base(). Функция bases() возвращает итератор, который порождает указатели на базовые классы объектов Type_info, а с помощью функции has_base() можно определить является ли заданный класс базовым для другого класса. Эти функции имеют необязательный параметр direct, который показывает, следует ли рассматривать все базовые классы ( direct=0 ), или только прямые базовые классы ( direct=1 ). Наконец, как описано ниже, с помощью функций get_info() и info() можно получить динамическую информацию о типе для самого класса Type_info.

Здесь средство динамических запросов о типе сознательно реализуется с помощью совсем простых классов. Так можно избежать привязки к определенной библиотеке. Реализация в расчете на конкретную библиотеку может быть иной. Можно, как всегда, посоветовать пользователям избегать излишней зависимости от деталей реализации.

Функция has_base() ищет базовые классы с помощью имеющегося в Type_info списка базовых классов. Хранить информацию о том, является ли базовый класс частным или виртуальным, не нужно, поскольку все ошибки, связанные с ограничениями доступа или неоднозначностью, будут выявлены при трансляции.

class base_iterator {
  short i;
  short alloc;
  const Type_info* b;
public:
  const Type_info* operator() ();
  void reset() { i = 0; }

  base_iterator(const Type_info* bb, int direct=0);
  ~base_iterator() { if (alloc) delete[] (Type_info*)b; }
               };

В следующем примере используется необязательный параметр для указания, следует ли рассматривать все базовые классы ( direct==0 ) или только прямые базовые классы ( direct==1 ).

base_iterator::base_iterator(const Type_info* bb, int direct)
{
  i = 0;

  if (direct) { // использование списка прямых базовых классов
     b = bb;
     alloc = 0;
     return;
  }

  // создание списка прямых базовых классов:

  // int n = число базовых
  b = new const Type_info*[n+1];
  // занести базовые классы в b

  alloc = 1;
  return;
}

const Type_info* base_iterator::operator() ()
{
  const Type_info* p = &b[i];
  if (p) i++;
  return p;
}

Теперь можно задать операции запросов о типе с помощью макроопределений:

#define static_type_info(T)  T::info()

#define ptr_type_info(p)   ((p)->get_info())
#define ref_type_info(r)   ((r).get_info())

#define ptr_cast(T,p) \
   (T::info()->can_cast((p)->get_info()) ? (T*)(p) : 0)
#define ref_cast(T,r) \
   (T::info()->can_cast((r).get_info()) \
       ? 0 : throw Bad_cast(T::info()->name()), (T&)(r))

Предполагается, что тип особой ситуации Bad_cast (Ошибка_приведения) описан так:

class Bad_cast {
  const char* tn;
  // ...
public:
  Bad_cast(const char* p) : tn(p) { }
  const char* cast_to() { return tn; }
  //  ...
};

В разделе \S 4.7 было сказано, что появление макроопределений служит сигналом возникших проблем. Здесь проблема в том, что только транслятор имеет непосредственный доступ к литеральным типам, а макроопределения скрывают специфику реализации. По сути для хранения информации для динамических запросов о типах предназначена таблица виртуальных функций. Если реализация непосредственно поддерживает динамическую идентификацию типа, то рассматриваемые операции можно реализовать более естественно, эффективно и элегантно. В частности, очень просто реализовать функцию ptr_cast(), которая преобразует указатель на виртуальный базовый класс в указатель на его производные классы.

< Лекция 12 || Лекция 13: 1234567891011
Равиль Ярупов
Равиль Ярупов
Привет !
Федор Антонов
Федор Антонов
Оплата и обучение
Роман Островский
Роман Островский
Украина
Оксана Пагина
Оксана Пагина
Россия, Москва