Как следует из названия, я ищу быстрый способ проверки типов во время выполнения. Чтобы проиллюстрировать мою проблему, представьте, что у вас есть иерархия классов, подобная следующей:
Base
/ \
A D
/ \ / \
C B F E
\ /
G
Моя программа хранит все экземпляры любого класса в одном списке как Base_ptr, потому что все эти классы имеют общие задачи. Теперь в какой-то момент некоторые производные классы должны будут знать о существовании экземпляра другого класса. Пока все хорошо, я знаю о dynamic_cast и операторе typeid (), но оба имеют некоторые недостатки мэра:
Идеальным решением было бы какое-то «isTypeOrSubtype» -тестирование и только приведение, если этот тест завершится успешно. У меня есть собственный подход с некоторыми определениями макросов и предварительно вычисленными хэшами имен классов, но он очень уродливый и трудно обслуживаемый. Поэтому я ищу более чистый и быстрый способ динамической проверки типов и подтипов, который может проверять намного больше, чем 20 миллионов раз в секунду.
Я написал ответ на свой вопрос, так как это другой подход, позволяющий избежать RTTI, но нет реального ответа на быстрый способ динамической проверки типа / подтипа.
Это все еще не чистое решение, но лучшее, что я мог придумать до сих пор.
Если у каждого класса в этой иерархии есть следующие характеристики, я могу пропустить большую часть RTTI.
static SecureVector<[class]*> s_Instances;
где SecureVector<T>
является потокобезопасным вектором. s_Instances.push_back(this);
должен вызываться, чтобы отслеживать вновь созданный экземпляр этого классаs_Instances.erase(this);
должен быть вызван, чтобы удалить эту ссылку экземплярыstatic const SecureVector<[class]*>& Instances() { return s_Instances; }
получить неизменяемый вектор, содержащий все экземпляры этого или любого производного классаПри каждом вызове конструктора экземпляр добавляет себя в свой список экземпляров. Когда производные классы вызывают свой супер-конструктор, супер-класс добавляет себя в соответствующий список экземпляров.
Например. если бы я случайно создал 100 экземпляров в вышеприведенной иерархии, всегда было бы 100 записей в моем Base
учебный класс Instances()
вектор.
В коде это будет выглядеть так:
class Base
{
static SecureVector<Base*> s_Instances; // 1. condition
public:
Base()
{
s_Instances.push_back(this); // 2. condition
}
~Base()
{
s_Instances.erase(this); // 3. condition
}
static const SecureVector<Base*>& Instances() { return s_Instances; } // 4. condition
};
Это все еще такой же обходной путь, поскольку четыре условия должны быть добавлены вручную (или макросом или чем-то вроде этого).
Некоторое время назад я использовал что-то вроде этого:
// the actual type is irrelevant, const char*, int, ...
// but const char* is great for debugging, when it contains the actual class name
typedef const char* TypeId;
class Base {
// actually the type id is not the value, but its memory location !
// the value is irrelevant (but its a good idea to set it to the class name)
static TypeId s_myTypeId;
public:
static TypeId* getClassType() { return &s_myTypeId; }
virtual TypeId* getInstanceType() { return &s_myTypeId; }
static TypeId* getClassBaseType() { return NULL; }
virtual TypeId* getInstanceBaseType() { return NULL; }
virtual bool isType( TypeId* type ) { return type==getInstanceType(); }
virtual bool isTypeOrSubType( TypeId* type ) { return isType(type); }
};template< class MyBase >
class TBase : public MyBase {
// actually the type id is not the value, but its memory location !
// the value is irrelevant (but its a good idea to set it to the class name)
static TypeId s_myTypeId;
public:
static TypeId* getClassType() { return &s_myTypeId; }
virtual TypeId* getInstanceType() { return &s_myTypeId; }
static TypeId* getClassBaseType() { return MyBase::getClassType(); }
virtual TypeId* getInstanceBaseType() { return MyBase::getInstanceType(); }
virtual bool isType( TypeId* type ) { return type==getInstanceType(); }
virtual bool isTypeOrSubType( TypeId* type ) { return isType(type) || MyBase::isTypeOrSubType(type); }
};
// by deriving from TBase<Base>, a new instantiation of s_myTypeId was created,
// so the class now has its very own unique type id,
// and it inherited all the type resolution magic
class A : public TBase<Base> {
};
// NOTE: class B must not derive directly from A, but from TBase<A>
// imagine a hidden class between B and A,
// actually B inherits from the TBase<A> instantiation, which in turn inherits from A
class B : public TBase<A> {
};
// you will also need to instantiate the static members
// hereby the memory location will be reserved,
// and on execution that memory location becomes the unique type id
#define IMPLEMENT_RTTI(CL) TypeId CL::s_myTypeId = STRFY(CL)
// one per class per source file:
IMPLEMENT_RTTI(Base);
IMPLEMENT_RTTI(A);
IMPLEMENT_RTTI(B);
// example usage:
A a;
B b;
b.getInstanceType()==B::getClassType(); // TRUE
b.getInstanceBaseType()==A::getClassType(); // TRUE
B::getClassBaseType()==A::getClassType(); // TRUE
b.isType( B::getClassType() ); // TRUE
b.isType( A::getClassType() ); // FALSE
b.isTypeOrSubType( B::getClassType() ); // TRUE
b.isTypeOrSubType( A::getClassType() ); // TRUE
b.isTypeOrSubType( Base::getClassType() ); // TRUE
Это безопасно, быстро и просто в использовании. Вы просто должны соблюдать два правила:
class X
, но наследовать от TBase<X>
,IMPLEMENT_RTTI(Me)
в исходный файл.Есть один недостаток: он еще не поддерживает множественное наследование. Но это было бы возможно с несколькими изменениями.
И, вероятно, TypeId
тип должен быть составлен как typedef const char* TypeLoc
а также typedef TypeLoc* TypeId
, Может быть, просто вопрос вкуса.
Если ваша программа знает обо всех подтипах, которые будут проверяться, вы можете использовать виртуальный интерфейс, который возвращает указатель на подтип. Как отмечено в комментариях и комментариях, это не самый гибкий подход, поскольку он требует, чтобы базовый класс знал все производные классы. Тем не менее, это очень быстро. Таким образом, существует компромисс между гибкостью и производительностью.
class Base {
//...
virtual A * is_A () { return 0; }
virtual B * is_B () { return 0; }
//...
template <typename MAYBE_DERIVED>
MAYBE_DERIVED * isTypeOrSubtype () {
//...dispatch to template specialization to call is_X()
}
};
//...
class A : virtual public Base {
//...
A * is_A () { return this; }
};
На IDEONE предложенная техника в 20-50 раз быстрее, чем используя динамическое приведение.1 Реализация использует макросы, чтобы разрешить добавление нового класса в одном месте, и после этого соответствующие расширения базового класса выполняются автоматически.
(1) — Я изначально разогнал его примерно в 100 раз быстрее, но это было без
isTypeOrSubtype()
метод-обёртка, который я добавил для симуляции нужного интерфейса.
Если гибкость имеет более высокое значение, чем производительность, то чуть менее производительным решением является использование map
связывать типы и соответствующие значения указателя (наличие значений указателя устраняет необходимость в динамическом приведении). map
экземпляр поддерживается в базовом классе, а ассоциации создаются конструкторами подклассов. Будь регулярным map
или unordered_map
Используется будет зависеть от того, сколько подклассов виртуально наследуют базовый класс. Я бы предположил, что цифры будут маленькими, поэтому регулярный map
должно хватить.
class Base {
std::map<const char *, void *> children_;
//...
template <typename MAYBE_DERIVED>
MAYBE_DERIVED * isTypeOrSubtype () {
auto x = children_.find(typeid(MAYBE_DERIVED).name());
return ((x != children_.end())
? static_cast<MAYBE_DERIVED *>(x->second)
: 0);
}
};
//...
class A : virtual public Base {
//...
A () { children_[typeid(A).name()] = this; }
//...
};
На IDEONE это второе предложение в 10-30 раз быстрее используя динамическое приведение. Я не думаю, что IDEONE компилируется с оптимизацией, поэтому я ожидаю, что время будет ближе к первому предложению о сборке. Механизм как реализовано использует typeid(...).name()
как ключ к карте.2
(2) — Это предполагает, что
typeid(...).name()
возвращает что-то похожее на строковый литерал и всегда возвращает один и тот же указатель строки при работе с одним и тем же типом. Если ваша система не ведет себя таким образом, вы можете изменитьmap
взятьstd::string
вместо этого, но производительность будет ухудшена.
dynamic_cast
будет работать чудесно для этого!
Base *instance = //get the pointer from your collection;
A* ap = dynamic_cast<A*>(instance);
D* dp = dynamic_cast<D*>(instance);
if(ap) {
//instance is an A or a subclass of A
}
if(dp) {
//instance is a D or a subclass of D
}
Это будет работать и для более конкретных проверок. Таким образом, вы можете проверить любой тип, который вы хотите.