实现类的标识符或使用dynamic_cast

时间:2015-12-09 08:16:35

标签: c++ inheritance polymorphism dynamic-cast

我的问题与What's the point of IsA() in C++?有关。我有一个性能关键代码,它包含某个特定函数的特定函数处理,其中只有基指针可用。检查我们所拥有的派生类的最佳方法是什么?我编写了两个选项,在第二个选项中,我可以删除Animal_type枚举和get_type()函数。

#include <iostream>

enum Animal_type { Dog_type, Cat_type };

struct Animal
{
    virtual Animal_type get_type() const = 0;
};

struct Dog : Animal
{
    void go_for_walk() const { std::cout << "Walking. Woof!" << std::endl; }
    Animal_type get_type() const { return Dog_type; }
};

struct Cat : Animal
{
    void be_evil() const { std::cout << "Being evil!" << std::endl; }
    Animal_type get_type() const { return Cat_type; }
};

void action_option1(Animal* animal)
{
    if (animal->get_type() == Dog_type)
        dynamic_cast<Dog*>(animal)->go_for_walk();
    else if (animal->get_type() == Cat_type)
        dynamic_cast<Cat*>(animal)->be_evil();
    else
        return;
}

void action_option2(Animal* animal)
{
    Dog* dog = dynamic_cast<Dog*>(animal);
    if (dog)
    {
        dog->go_for_walk();
        return;
    }

    Cat* cat = dynamic_cast<Cat*>(animal);
    if (cat)
    {
        cat->be_evil();
        return;
    }

    return;
}

int main()
{
    Animal* cat = new Cat();
    Animal* dog = new Dog();

    action_option1(cat);
    action_option2(cat);

    action_option1(dog);
    action_option2(dog);

    return 0;
}

3 个答案:

答案 0 :(得分:2)

我想引用您引用的问题的接受答案:

  

在现代C ++中没有任何意义。

对于您的示例,最简单的解决方案是使用动态分派:

struct Animal {
    virtual void action() = 0;
};

struct Dog{
    virtual void action()  { std::cout << "Walking. Woof!" << std::endl; }
};

struct Animal {
    virtual void action()  { std::cout << "Being evil!" << std::endl; }
};

int main()
{
    Animals* a[2] = {new Cat(), new Dog()};
    a[0]->action();
    a[1]->action();
    delete a[0];
    delete a[1];
    return 0;
 }

对于更复杂的场景,您可以考虑设计模式,例如策略,模板方法或访问者。

如果这确实是一个性能瓶颈,那么将DogCat声明为final可能会有所帮助。

答案 1 :(得分:2)

它在很大程度上取决于 性能关键代码的性能关键程度。我已经看到过甚至动态调度虚拟功能的设置成本太高,所以如果你在这样的领域,忘掉dynamic_cast并手工制作东西。

不过,我会假设您可以使用一两个虚拟电话。您可能希望避开dynamic_cast,因为这通常比动态调度慢得多。

现在,你有来自公共基础的 N 类和代码中的 M 点,你需要根据具体的派生类做出决定。问题是: N,M 中哪一个更有可能在未来发生变化?您是否更有可能添加新的派生类,或引入类型决策重要的新点?这个答案将为您确定最佳设计。

如果您要添加新类,但类型区分位置的数量是固定的(理想情况下也很小),枚举方法将是最佳选择。只需使用static_cast代替dynamic_cast;如果您知道实际的运行时类型,则不需要访问RTTI来为您进行转换(除非涉及虚拟基础和更深层的继承层次结构)。

另一方面,如果列表类是固定的,但是可能会引入新的类型区分操作(或者如果它们的维护太多而无法维护),请考虑Visitor pattern代替。为您的Animal课程提供虚拟访问者接受功能:

virtual void accept(AnimalVisitor &v) = 0;

struct AnimalVisitor
{
  virtual void visit(Dog &dog) = 0;
  virtual void visit(Cat &cat) = 0;
};    

然后,每个派生类都将实现它:

void Dog::accept(AnimalVisitor &v)
{
  v.visit(*this);
}

void Cat::accept(AnimalVisitor &v)
{
  v.visit(*this);
}

您的操作只会使用它:

void action(Animal *animal)
{
  struct Action : AnimalVisitor
  {
    void visit(Dog &d) override { d.go_for_walk(); }
    void visit(Cat &c) override { c.be_evil(); }
  };

  AnimalVisitor v;

  animal->accept(v);
}

如果您要添加新的派生类和新操作,可以向上面的访问者添加非抽象函数,以便不需要了解新类的现有代码没有休息:

struct AnimalVisitor
{
  virtual void visit(Dog &d) = 0;
  virtual void visit(Cat &c) = 0;
  virtual void visit(Parrot &p) {}
};

答案 2 :(得分:1)

您的第一个选项会更快,但只有在您修复了错误的dynamic_cast(它应该是static_cast)时才会更快:

void action_option1_fixed(Animal* animal)
{
    if (animal->get_type() == Dog_type)
        static_cast<Dog*>(animal)->go_for_walk();
    else if (animal->get_type() == Cat_type)
        static_cast<Cat*>(animal)->be_evil();
}

get_type()上使用手动调度的重点是,它允许您避免在C ++运行时中对__dynamic_cast进行昂贵的调用。一旦你将这个调用带入运行时,你就输了。

如果您在finalDog上使用Cat限定符(即,您知道的程序中的每个类都不会有子类),那么将有足够的信息知道

dynamic_cast<Dog*>(animal)

可以实现为简单的指针比较;但遗憾的是(截至2017年)GCC和Clang都没有实施这样的优化。您可以使用C ++ get_type运算符手动执行优化,而无需引入typeid方法:

void action_option3(Animal* animal)
{
    static_assert(std::is_final_v<Dog> && std::is_final_v<Cat>, "");
    if (typeid(*animal) == typeid(Dog))
        static_cast<Dog*>(animal)->go_for_walk();
    else if (typeid(*animal) == typeid(Cat))
        static_cast<Cat*>(animal)->be_evil();
}

使用clang++ -std=c++14 -O3 -S进行编译应该会向您展示第三种方法的好处。

action_option1

开头
    movq    %rdi, %rbx
    movq    (%rbx), %rax
    callq   *(%rax)
    cmpl    $1, %eax
    jne     LBB0_1
    movq    __ZTI6Animal@GOTPCREL(%rip), %rsi
    movq    __ZTI3Dog@GOTPCREL(%rip), %rdx
    xorl    %ecx, %ecx
    movq    %rbx, %rdi
    callq   ___dynamic_cast
    movq    %rax, %rdi
    addq    $8, %rsp
    popq    %rbx
    popq    %rbp
    jmp     __ZNK3Dog11go_for_walkEv ## TAILCALL

action_option1_fixed将其改进为

    movq    %rdi, %rbx
    movq    (%rbx), %rax
    callq   *(%rax)
    cmpl    $1, %eax
    jne     LBB2_1
    movq    %rbx, %rdi
    addq    $8, %rsp
    popq    %rbx
    popq    %rbp
    jmp     __ZNK3Dog11go_for_walkEv ## TAILCALL

(请注意,在固定版本中,对__dynamic_cast的调用已经消失,仅用一个小指针数学代替。)

action_option2实际上比action_option1短,因为它不会在 __dynamic_cast之上添加虚拟调用,但它仍然很糟糕:

    movq    %rdi, %rbx
    testq   %rbx, %rbx
    je      LBB1_3
    movq    __ZTI6Animal@GOTPCREL(%rip), %rsi
    movq    __ZTI3Dog@GOTPCREL(%rip), %rdx
    xorl    %ecx, %ecx
    movq    %rbx, %rdi
    callq   ___dynamic_cast
    testq   %rax, %rax
    je      LBB1_2
    movq    %rax, %rdi
    addq    $8, %rsp
    popq    %rbx
    popq    %rbp
    jmp     __ZNK3Dog11go_for_walkEv ## TAILCALL

这是action_option3。它足够小,我可以在这里粘贴整个函数定义,而不是摘录:

__Z14action_option3P6Animal:
    testq   %rdi, %rdi
    je      LBB3_4
    movq    (%rdi), %rax
    movq    -8(%rax), %rax
    movq    8(%rax), %rax
    cmpq    __ZTS3Dog@GOTPCREL(%rip), %rax
    je      LBB3_5
    cmpq    __ZTS3Cat@GOTPCREL(%rip), %rax
    je      LBB3_6
    retq
LBB3_5:
    jmp     __ZNK3Dog11go_for_walkEv ## TAILCALL
LBB3_6:
    jmp     __ZNK3Cat7be_evilEv     ## TAILCALL
LBB3_4:
    pushq   %rbp
    movq    %rsp, %rbp
    callq   ___cxa_bad_typeid

最后的__cxa_bad_typeid因为可能是animal == nullptr的情况。您可以通过使类型为Animal&而不是Animal*的参数来消除这种错误,以便编译器知道它是非空的。

我尝试在函数顶部添加此行:

if (animal == nullptr) __builtin_unreachable();

但遗憾的是,Clang对typeid的实施没有接受这一暗示。