【问题标题】:C++ fast dynamic type/subtype checkC++ 快速动态类型/子类型检查
【发布时间】:2013-09-14 21:08:10
【问题描述】:

正如标题所示,我正在寻找一种快速的运行时类型检查方法。为了说明我的问题,假设您有一个如下所示的类层次结构:

      Base
     /     \
    A       D
   / \     / \
  C   B   F   E
       \ /
        G

我的程序将任何类的所有实例保存在一个列表中作为 Base_ptr,因为所有这些类都共享共同的任务。现在在某些时候,一些派生类需要知道另一个类的实例的存在。到目前为止一切顺利,我知道 dynamic_cast 和 typeid()-operator,但两者都有一些主要的缺点:

  1. 如果类型不兼容,dynamic_cast 会消耗大量处理时间(例如,尝试将 E 实例强制转换为 C)
  2. typeid() 在“isTypeOrSubtype”情况下不起作用,例如您需要 D 的所有实例或从 D 派生的实例(因此 Es、Fs 和 Gs 也是如此)

如果此测试成功返回,理想的解决方案将是某种“isTypeOrSubtype”测试并且仅进行强制转换。我有一个自己的方法,其中包含一些宏定义和预先计算的类名哈希,但它非常丑陋且难以维护。所以我正在寻找一种更干净、更快速的动态类型和子类型检查方法,每秒可以检查超过 2000 万次。

【问题讨论】:

  • 如果您真的“正在寻找一种更清洁、更快速的动态类型和子类型检查方法,每秒可以检查超过 2000 万次”,那么您真的应该重新考虑您的设计!需要高性能类型信息检查的设计很糟糕。
  • 这可能是一个误解,因为我不打算经常检查。我确实需要某种测量来比较不同的方法,所以如果我通过让它们运行相同的测试并同时测量 10m 案例所花费的时间来比较两个或多个不同的设计。它需要执行得那么快,以免在某些类使用类型检查的情况下减慢其他组件的速度。
  • 所以,您“不打算经常检查”,而是“它需要执行得那么快,这样才不会减慢其他组件的速度……”?这仍然是相同的消息!我仍然建议您重新考虑您的设计并尽量避免使用 RTTI。
  • 如果你的程序基于运行时类型检查来执行业务逻辑,那么你就走错了方向。
  • 如果不在乎,为什么还要检查?

标签: c++ rtti typechecking


【解决方案1】:

如果您的程序知道将要测试的所有子类型,则可以使用返回指向子类型的指针的虚拟接口。正如 downvotes 和 cmets 所指出的,这不是最灵活的方法,因为它要求基类了解所有派生类。但是,它非常快。因此,需要在灵活性与性能之间进行权衡。

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 上,suggested techniqueusing dynamic cast 快 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 上,这个 second suggestionusing dynamic cast 快​​ 10 到 30 倍。我不认为 IDEONE 编译时会进行优化,所以我希望时间更接近于生产构建的第一个建议。实现的机制使用typeid(...).name() 作为映射的键。2

(2) - 这假定typeid(...).name() 返回类似于字符串文字的内容,并且在对相同类型进行操作时始终返回相同的字符串指针。如果您的系统不是这样,您可以修改map 以将std::string 作为键,但性能会下降。

【讨论】:

  • 我没有投反对票,但是拥有必须知道所有派生类的基类总是一个坏主意。
  • 没有投反对票,但这不是最动态的方式 - 每次添加新子类时都必须向每个子类添加一个函数。
  • @Quirliom:我同意,我已经更新了答案以反映这一点。
  • 我已经在我的程序的另一部分使用了稍微不同的方法来处理这个想法,我不会说这个想法总是不好的。但事实是这个想法仅限于你已经知道所有子类的情况,所以很可能是一个封闭的层次结构。我的方法只是使用子类作为函数参数,但这仍然不能解决我的上述问题。
  • @rootmenu:我同意,这种方法不适用于基类是导出库组件的公共接口的情况。但是,如果基类仅由在基类所属的同一封闭系统中实现的类使用,则层次结构是封闭的。
【解决方案2】:

前段时间我用过这样的东西:

// 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&lt;X&gt;继承,
  • 并将IMPLEMENT_RTTI(Me) 添加到源文件。

有一个缺点:它还不支持多重继承。但只要稍作改动,就可以实现。

可能TypeId 类型应该像typedef const char* TypeLoctypedef TypeLoc* TypeId 一样组成。也许只是品味问题。

【讨论】:

    【解决方案3】:

    我为自己的问题写了一个答案,因为这是一种避免 RTTI 的不同方法,但对动态类型/子类型检查的快速方法没有真正的答案。 这仍然不是一个干净的解决方案,但到目前为止我能想到的最好的解决方案。

    如果这个层次结构中的每个类都具有以下特征,我可以跳过大部分 RTTI。

    1. 每个类都应该有一个私有成员:static SecureVector&lt;[class]*&gt; s_Instances; 其中SecureVector&lt;T&gt; 是一个线程安全向量。
    2. 在每个构造函数结束时,应调用s_Instances.push_back(this);,以跟踪该类的新创建实例
    3. 在析构函数的开头,应调用s_Instances.erase(this);,以删除此实例引用
    4. 每个类都应该有一个公共函数:static const SecureVector&lt;[class]*&gt;&amp; Instances() { return s_Instances; } 以获取一个不可修改的向量,其中包含该类或任何派生类的所有实例

    它的作用是,每次调用构造函数时,实例都会将自己添加到自己的实例列表中。当派生类调用它们的超构造函数时,超类将自身添加到其各自的实例列表中。 例如。如果我在上述层次结构中随机创建 100 个实例,我的 BaseInstances() 向量中总会有 100 个条目。

    在代码中,这看起来像这样:

    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
    };
    

    这仍然是一种解决方法,因为必须手动添加四个条件(或通过宏或类似的东西)。

    【讨论】:

    • 我知道这如何帮助您跟踪所有实例,但您能否展示如何使用它来实现 isTypeOrSubtype()
    • 绝对错误的方向。在您真正需要类型检查的少数情况下,您会增加大量运行时开销。检查我的答案,它根本不会增加运行时开销。
    • @Frunsi:我看不出这会增加很多运行时开销。我很清楚这会为构造实例增加一点开销,但构造实例的频率不会像处理它们的频率那样频繁。
    • @rootmenu:我不明白这是如何让指向基类型的指针确定它是否是没有动态转换的派生类型之一。你能举例说明它是如何完成的吗?
    【解决方案4】:

    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
    }
    

    这也适用于更具体的检查。所以你可以检查你想要的任何类型。

    【讨论】:

    • 不知道我怎么错过了,我的错!
    猜你喜欢
    • 1970-01-01
    • 2016-12-01
    • 2018-07-10
    • 1970-01-01
    • 1970-01-01
    • 2021-01-15
    • 2010-11-23
    • 2019-05-20
    • 2021-12-25
    相关资源
    最近更新 更多