9

我正在用 C++ 编写一个游戏,它有大约 30 个不同的角色,每个角色都略有不同。我有一个主类用户,其中包含所有角色所需的所有数据。我的第一个实现只涉及枚举 30 个角色并进行适当处理,但现在我想知道将 User 作为基类并且每个角色都是从 User 继承的自己的类是否会更好。

我主要关心的是当有 30 多个类从单个基类继承时,多态方法调用的效率如何?我知道多态调用涉及虚拟表中的指针,但我不确定这是否意味着在整个表中线性搜索正确的方法,或者是否可以在恒定时间内完成。

如果有人可以评论具有许多继承类的多态方法调用的效率,我将不胜感激。

提前致谢!

4

3 回答 3

7

成本可以忽略不计。不管你有多少类,或者有多少继承级别,多态调用的成本是一个简单的加法——指向加号的指针vftable加上特定函数的偏移量(不是标准强制的,但在大多数情况下,如果不是全部,这是正确的实现)。

于 2012-04-15T23:31:12.387 回答
7

与其使用继承,不如使用组合。只需给您的“用户”类一个执行该角色的对象即可。所以你最终可能会得到 30 个“角色”类。但是它们不会继承'User',而是交给'User'使用(他们可以继承自己的基本抽象类来定义'Role'的接口)

或者如果它只是一个函数......你可能只是将它建模为一堆函数对象,然后将它们传递给用户。

于 2012-04-15T23:32:48.807 回答
0

不幸的是,Luchian Grigore的答案是不正确的。


在Diamond 继承的情况下是正确的。如果你有一个继承自 B 和 C 的类 D,它本身继承自 A。如果你调用 D 的函数,将对 vtable 进行添加操作以查找 D 引用。

 (e.g)                   in Memory
                          -------        Classes hierarchy
                     ---> |  A  |                A
the offset here      |    -------               / \
is the pointer   ->  |    | ... |              B   C    <- Diamond Inheritance
addition he is       |    -------              \   /        (usually very bad)
talking about        ---> |  D  |                D
                          -------

在您的情况下,对于多态调用,编译器必须对 vtable 进行查找(读取)才能获得您正在调用的正确函数。当 vtable 指向的数据没有被缓存时,情况会变得更糟。在这种情况下,您会遇到缓存未命中,这是非常昂贵的。

此外,每个类有一个 vtable,并且该类的每个对象共享同一个 vtable。请参阅在 C++ 中,什么是 vtable,它是如何工作的?


您问题的真正答案取决于您将在这 30 个类中的每一个之间交换多少次,以及您使用它们的频率。会循环使用吗?

您描述的解决方案通常用于解决此潜在问题。但实际上,它可能与多态调用一样快,具体取决于它的使用方式。

所以一般来说你可以选择多态方法而不用担心成本,因为它可以忽略不计。首先编写干净且可维护的代码,然后再进行优化。:)

编辑 :

由于在这个线程上讨论了实际的编译器代码,我决定运行一个示例来找出真正发生的事情。

下面你可以看到我使用的代码示例。

#include <stdlib.h>
#include <stdio.h>

class I
{
public:
    virtual void f(void) = 0;
};

class A : public I
{
public:
    void f(void)
    {
        printf("A\n");
    }
};


int main(int argc, char* argv[])
{
    __asm
    {
        int 3
    }

    A* pA = new A();

    __asm
    {
        nop
        nop
    }

    pA->f();

    __asm
    {
        nop
        nop
    }

    A a;
    a.f();

    __asm
    {
        nop
        nop
    }

    return 0;
}

然后,您可以看到示例的实际汇编代码(编译器如何解释它)。

int main(int argc, char* argv[])
{
    __asm
    {
        int 3
010E1010  int         3    
    }

    A* pA = new A();
010E1011  push        4    
010E1013  call        operator new (10E10A4h) 
010E1018  add         esp,4 
010E101B  test        eax,eax 
010E101D  je          main+17h (10E1027h) 
010E101F  mov         dword ptr [eax],offset A::`vftable' (10E2104h)
010E1025  jmp         main+19h (10E1029h) 
010E1027  xor         eax,eax 

    __asm
    {
        nop
010E1029  nop              
        nop
010E102A  nop              
    }

    pA->f();
010E102B  mov         edx,dword ptr [eax] 
010E102D  mov         ecx,eax 
010E102F  mov         eax,dword ptr [edx] 
010E1031  call        eax  

    __asm
    {
        nop
010E1033  nop              
        nop
010E1034  nop              
    }

    A a;
    a.f(); //Polymorphic call
010E1035  push        offset string "A\n" (10E20FCh) 
010E103A  call        dword ptr [__imp__printf (10E20ACh)]
010E1040  add         esp,4 

    __asm
    {
        nop
010E1043  nop              
        nop
010E1044  nop              
    }

    return 0;
010E1045  xor         eax,eax 
}

class A : public I
{
public:

    void f(void)
    {
        printf("A\n");
010E1000  push        offset string "A\n" (10E20FCh) 
010E1005  call        dword ptr [__imp__printf (10E20ACh)] 
010E100B  pop         ecx  
    }
于 2012-04-17T02:56:05.517 回答