【问题标题】:A standard way to avoid virtual functions避免虚函数的标准方法
【发布时间】:2014-05-13 09:55:29
【问题描述】:

我有一个库,里面有很多小对象,现在它们都有虚函数。它达到了这样的程度,即指向虚函数表的指针的大小可以超过对象中有用数据的大小(它通常可以只是一个带有单个float 的结构)。对象是稀疏图上数值模拟中的元素,因此不能轻易合并/等。

我不太关心虚函数调用的成本,而是存储成本。发生的事情是指向虚函数表的指针基本上是在降低缓存的效率。我想知道将类型 id 存储为整数而不是虚函数是否会更好。

我不能使用静态多态性,因为我的所有对象都在一个列表中,并且我需要能够对由索引选择的项目执行操作(这是一个运行时值 - 因此无法静态确定类型)。

问题是:在给定类型列表(例如在类型列表中)和类型索引的情况下,是否存在可以从接口动态调用函数的设计模式或通用算法?

接口已定义并且没有太大变化,但是将来会由库的(可能不太熟练的)用户声明新对象,这样做不需要很大的努力。性能是最重要的。可悲的是,没有 C++11。

到目前为止,我可能有一个愚蠢的概念证明:

typedef MakeTypelist(ClassA, ClassB, ClassC) TList; // list of types

enum {
    num_types = 3 // number of items in TList
};

std::vector<CommonBase*> uniform_list; // pointers to the objects
std::vector<int> type_id_list; // contains type ids in range [0, num_types)

template <class Op, class L>
class Resolver { // helper class to make a list of functions
    typedef typename L::Head T;

    // specialized call to op.Op::operator ()<T>(p)
    static void Specialize(CommonBase *p, Op op)
    {
        op(*(T*)p);
    }

    // add a new item to the list of the functions
    static void BuildList(void (**function_list)(CommonBase*, Op))
    {
        *function_list = &Specialize;
        Resolver<Op, typename L::Tail>::BuildList(function_list + 1);
    }
};

template <class Op>
class Resolver<Op, TypelistEnd> { // specialization for the end of the list
    static void BuildList(void (**function_list)(CommonBase*, Op))
    {}
};

/**
 * @param[in] i is index of item
 * @param[in] op is a STL-style function object with template operator ()
 */
template <class Op>
void Resolve(size_t i, Op op)
{
    void (*function_list[num_types])(CommonBase*, Op);
    Resolver<Op, TList>::BuildList(function_list);
    // fill the list of functions using the typelist

    (*function_list[type_id_list[i]])(uniform_list[i], op);
    // call the function
}

我还没有查看程序集,但我相信如果设为静态,函数指针数组的创建几乎可以免费进行。另一种选择是使用在 typelist 上生成的二叉搜索树,这将启用内联。

【问题讨论】:

  • 旁注:您也可以将 type_id_list 设为 vector&lt;uint8_t&gt; 或类似的,以获得更好的缓存使用率,> 255 种行为即使对于最复杂的模拟也很多(如果您确实超过了它,还有uint16_t)。
  • 当您用虚拟指针所需的空间换取enum(本质上是int)的空间时,您几乎不会获得太多收益。 Flyweight pattern 让您假装许多小的原语是真正的对象,但我不确定如何在您的对象是多态的情况下应用它,并且您宁愿不保留额外的指针。
  • size_t 可以,但指针不行吗?如果您可以接受 32 位的编译,那么它们很可能是相同的大小。
  • 能否将类型信息编码到索引中?因此,使用给定的索引,您可以获得数据对象和调用该对象的函数。
  • @delnan 是的,实际上整型时会根据num_types 的值选择type_id_list 的整数类型。

标签: c++ design-patterns virtual-functions typelist


【解决方案1】:

我最终使用了我在问题中概述的“thunk table”概念。对于每个操作,都有一个 thunk 表的单个实例(它是静态的并且通过模板共享 - 因此编译器将自动确保每个操作类型只有一个表实例,而不是每个调用)。因此,我的对象没有任何虚函数

最重要的是 - 使用简单函数指针而不是虚拟函数所带来的速度增益是可以忽略的(但也不慢)。提高速度的是实现决策树并静态链接所有函数 - 这将一些计算密集度不高的代码的运行时间提高了大约 40%

一个有趣的副作用是能够拥有“虚拟”模板函数,这通常是不可能的。

我需要解决的一个问题是我的所有对象都需要有一些接口,因为它们最终会被函子以外的一些调用访问。我为此设计了一个独立的外观。外观是一个虚拟类,声明对象的接口。分离的外观是这个虚拟类的实例,专门用于给定的类(对于列表中的所有类,operator [] 返回所选项目类型的分离外观)。

class CDetachedFacade_Base {
public:
    virtual void DoStuff(BaseType *pthis) = 0;
};

template <class ObjectType>
class CDetachedFacade : public CDetachedFacade_Base {
public:
    virtual void DoStuff(BaseType *pthis)
    {
        static_cast<ObjectType>(pthis)->DoStuff();
        // statically linked, CObjectType is a final type
    }
};

class CMakeFacade {
    BaseType *pthis;
    CDetachedFacade_Base *pfacade;

public:
    CMakeFacade(BaseType *p, CDetachedFacade_Base *f)
        :pthis(p), pfacade(f)
    {}

    inline void DoStuff()
    {
        f->DoStuff(pthis);
    }
};

要使用它,需要这样做:

static CDetachedFacade<CMyObject> facade;
// this is generated and stored in a templated table
// this needs to be separate to avoid having to call operator new all the time

CMyObject myobj;
myobj.DoStuff(); // statically linked

BaseType *obj = &myobj;
//obj->DoStuff(); // can't do, BaseType does not have virtual functions

CMakeFacade obj_facade(obj, &facade); // choose facade based on type id
obj_facade.DoStuff(); // calls CMyObject::DoStuff()

这允许我在代码的高性能部分使用优化的 thunk 表,并且仍然具有多态行为对象,以便能够在不需要性能的地方方便地处理它们。

【讨论】:

    【解决方案2】:

    CRTP 是虚拟函数的编译时替代方案:

        template <class Derived> 
        struct Base
        {
            void interface()
            {
                // ...
                static_cast<Derived*>(this)->implementation();
                // ...
            }
    
            static void static_func()
            {
                // ...
                Derived::static_sub_func();
                // ...
            }
        };
    
        struct Derived : Base<Derived>
        {
            void implementation();
            static void static_sub_func();
        };
    

    它依赖于成员的定义在被调用之前不会被实例化的事实。所以 Base 应该只在其成员函数的定义中引用 Derived 的任何成员,而不是在原型或数据成员中

    【讨论】:

    • 您希望如何将此类对象存储在单个列表中并通过通用接口使用它们?
    • 您可以拥有 Base 指针列表并调用 Base 公开的任何函数
    • 哪个 Base?有很多:Base&lt;Derived&gt;, Base&lt;Derived2&gt;, Base&lt;Derived3&gt;, ... 这些并没有共同的超类型。您当然可以引入一个,但是要获得动态多态性,您仍然需要创建这些函数virtual,然后您又回到了第一方。充其量您将所有实现共有的一些预处理或后处理提取到Base&lt;T&gt;,这很好,但与问题无关。
    • 是的,正如 delnan 正确所说,在这种情况下很难使用,因为多态是动态的,不能轻易地简化为静态。不过感谢您的努力 (+1)。
    • "我不能使用静态多态"
    猜你喜欢
    • 2011-06-15
    • 1970-01-01
    • 2010-11-12
    • 1970-01-01
    • 1970-01-01
    • 2014-04-03
    • 2011-10-09
    • 1970-01-01
    • 2013-04-09
    相关资源
    最近更新 更多