С++: эмуляция RTTI

У меня есть иерархия классов как эта:

class A        { }                            //
class AA  : A  { }                            //          A
class AAA : AA { }                            //        /   \
class AAB : AA { }                            //      AA     AB
class AB  : A  { }                            //     / \     / \
class ABA : AB { }                            //   AAA AAB ABA ABB
class ABB : AB { }                            //

Я хотел бы эмулировать RTTI (без его использования, конечно) для этой иерархии, таким образом, что, учитывая указатель/ссылку на A, я могу узнать его фактический тип (аналогично тому, что typeid is), как целое число, идентифицирующее класс.

Кроме того, мне хотелось бы, чтобы набор целых чисел, идентифицирующих мои типы, был смежным и от 0 до N-1 (от 0 до 6 в моем примере):

class A        { virtual int t(){return 0;} } //
class AA  : A  { virtual int t(){return 1;} } //            A(0)
class AAA : AA { virtual int t(){return 2;} } //          /      \
class AAB : AA { virtual int t(){return 3;} } //      AA(1)       AB(4)
class AB  : A  { virtual int t(){return 4;} } //     /   \        /    \
class ABA : AB { virtual int t(){return 5;} } // AAA(2) AAB(3) ABA(5) ABB(6)
class ABB : AB { virtual int t(){return 6;} } //

(порядок не имеет значения: A::t может возвращать 3 и AAB::t 0, например.


Можно ли дать компилятору присвоение индексов моим классам?

Я думаю, что CRTP мог бы помочь мне; что-то вроде:

class X : A, AssignFirstAvailableIndex< X > { }

но я недостаточно хорош с шаблонами. Как я мог реализовать этот класс шаблонов AssignFirstAvailableIndex?

(конечно, компилятор может видеть все классы во время компиляции)

Ответ 1

Существует стандартный метод для реализации того, что вам нужно. Стандартные языковые грани используют его для идентификации себя. Рассмотрим рассмотрение стандартного заголовка "locale".

class Base {
  public:
  // Being constructed contains a new unique identifier
  class Id {
    // An id factory returns a sequence of nonnegative ints
    static int allocate() {
      static int total = 0;
      return total++;
    }
    int _local;
    public:
    Id(): _local(allocate()) {}
    int get() const {return _local;}
  };
  //Child classes should make this function return an id generated by Base::Id constructed as static member.
  virtual int id() const = 0;
};

class Child1{
  public:
  static const Base::Id _id; 
  virtual int id() { return _id.get(); }
};

class Child2 {
  public:
  static const Base::Id _id; 
  virtual int id() { return _id.get(); }
};

class Child3 {
  public:
  static const Base::Id _id; 
  virtual int id() { return _id.get(); }
};

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

Ответ 2

Может быть, что-то вроде этого? (синтаксис, вероятно, ошибочен повсюду, но вы получаете идею)

class A {
  virtual int t() { return 0; }
}

template <bool AB>
class AX<AB>: A {
  virtual int t() { return AB ? 1 : 2; }
};

template <bool AB2>
template <bool AB>
class AXX<AB2>: AX<AB> {
  virtual int t() { return AX<AB>::t() * 2 + (AB2 ? 1 : 2); }
}

... Но серьезно, пожалуйста просто используйте RTTI.

Ответ 3

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

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

Во-первых, укажите объект, представляющий тип, и используйте типичный подход, сделанный вручную - RTTI: у каждого класса есть статический экземпляр этого объекта и его виртуальная функция get-type-info возвращает указатель на этот объект. Поэтому у каждого класса будет немного кода, например:

static TypeInfo ms_type_info;
virtual const TypeInfo *GetTypeInfo() const {
    return &ms_type_info;
}

И вы определяете информацию о типе, помещая в раздел <<whatever you info you want>> любую информацию, которую хранит TypeInfo, чтобы сделать ее лучше, чем RTTI компилятора;

TypeInfo WhateverClass::ms_type_info(<<whatever info you want>>);

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

Сама структура TypeInfo выглядит примерно так:

struct TypeInfo {
    int type_index;
    TypeInfo *next;
    TypeInfo(<<whatever>>) {<<see below>>}
};

Если читатель предпочел бы получать и устанавливать функции, он мог бы иметь эти.

Объект TypeInfo должен быть статичным для класса, а не для функции, поскольку целью является создание списка всех TypeInfos. У вас есть два варианта. Автоматическое - иметь каждый TypeInfo, в конструкторе, который был оставлен пустым выше, добавить себя в какой-то глобальный связанный список; другой - иметь большую функцию, которая добавляет те, которые она хочет к глобальному списку вручную.

Затем, при запуске, пропустите объекты TypeInfo и назначьте каждый индекс. Что-то вроде этого будет делать, предполагая, что там TypeInfo *g_first_type_info, который указывает на информацию первого типа в списке:

int next_type_index=0;
for(TypeInfo *ti=g_first_type_info;ti;ti=ti->next)
    ti->type_index=next_type_index++;

Теперь, когда вам нужен идентификатор целого числа, вы можете легко его скачать:

object->GetTypeInfo()->type_index;

Теперь вы можете реализовать t достаточно легко:

virtual int t() const {
    return ms_type_info.type_index;
}

Полное раскрытие проблем, о которых я могу думать:

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

  • Если вам нравится делать все в глобальных конструкторах до начала main, вы можете отклеиться, так как объекты TypeInfo являются просто обычными глобальными объектами и (в этой ситуации в любом случае) неправильно подготовлены для используйте так или иначе, пока не будут присвоены индексы типов.

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

Ответ 4

Я успешно использовал описанный здесь метод: http://www.flipcode.com/archives/Run_Time_Type_Information.shtml. В основном каждый класс, который требует RTTI, имеет статическую функцию, которая возвращает свой тип rtti. преимущество использования структуры для типа заключается в том, что вы можете добавлять функции в свою структуру rtti.

Я изменил этот подход, чтобы позволить такие вещи, как component->IsOfType(CollisionComponent::GetClass()).

Я также расширил структуру RTTI, чтобы предоставить имя класса, и теперь я могу позвонить component->IsOfType("CollisionComponent") Без включения CollisionComponent.h.

Этот подход очень удобен в сочетании с этим созданием динамического класса. Я могу создавать и идентифицировать классы С++ в сценариях и создавать экземпляры большого количества различных компонентов, которые загружаются только тогда, когда OS/hardware поддерживает необходимые типы. Я также могу перенастроить типы загружаемых компонентов в зависимости от версии сервера. Например, сервер может разрешить использование "CollisionComponent_V2_0", а другой - принудительно использовать "CollisionComponent_Proxy_V2"