简体   繁体   English

实现类的标识符或使用dynamic_cast

[英]Implement an identifier for a class or use dynamic_cast

My question relates to What's the point of IsA() in C++? 我的问题与C ++中IsA()有什么意义? . I have a performance critical code that contains at a certain spot treatment of specific functions from derived classes, where only the base pointer is available. 我有一个性能关键代码,其中某些地方包含派生类中特定功能的处理,其中只有基指针可用。 What is the best way of checking which derived class we have? 检查我们拥有哪个派生类的最佳方法是什么? I have coded up two options, in the second option I could eliminate the Animal_type enum and the get_type() function. 我已经编写了两个选项,在第二个选项中,我可以消除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;
}

I want to quote the accepted answer to the question you are citing: 我想引用您所引用问题的可接受答案:

In modern C++ there is no point. 在现代C ++中,没有意义。

For your example, the easiest solution is to use dynamic dispatch: 对于您的示例,最简单的解决方案是使用动态调度:

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

For more complex scenarios, you may consider design patterns such as Strategy, Template Method or Visitor. 对于更复杂的场景,您可以考虑设计模式,例如策略,模板方法或访客。

If this really is a performance bottlenect, it may help to declare Dog and Cat as final . 如果这确实是性能瓶颈,则可以将Dog and Cat声明为final

It largely depends on how performance-critical your performance-critical code is. 这在很大程度上取决于你的性能关键代码性能关键是。 I've seen setups where even dynamic dispatch of virtual functions was too costly, so if you're in such territory, forget about dynamic_cast and hand-craft something. 我见过甚至虚拟函数的动态分派成本都很高的设置,因此,如果您处在这样的领域,请别忘了dynamic_cast和手工制作的东西。

I will assume you're fine with a virtual call or two, though. 不过,我假设您可以进行一次或两次虚拟通话。 You will probably want to steer clear of dynamic_cast , as that is usually much slower than a dynamic dispatch. 您可能希望避开dynamic_cast ,因为通常这比动态调度要慢得多。

Right now, you have N classes derived from the common base and M points in code where you need to take a decision based on the concrete derived class. 现在,您有N个从公共基数派生的类和M个代码点,您需要根据具体派生类做出决策。 The question is: which of N, M is more likely to change in the future? 问题是:未来N,M中哪个更可能发生变化? Are you more likely to add new derived classes, or introduce new points where type-decision matters? 您是否更有可能添加新的派生类,或在类型决定很重要的地方引入新的观点? This answer will determine the best design for you. 该答案将为您确定最佳设计。

If you're going to add new classes, but the number of type-discriminating places is fixed (and ideally small as well), the enumeration approach would be the best choice. 如果您要添加新的类,但是区分类型的位置的数量是固定的(理想情况下也要很小),那么枚举方法将是最佳选择。 Just use a static_cast instead of a dynamic_cast ; 只需使用static_cast而不是dynamic_cast if you know the actual runtime type, you don't need to access RTTI to do the conversion for you (unless virtual bases and a deeper inheritance hierarchy are involved). 如果您知道实际的运行时类型,则无需访问RTTI即可为您进行转换(除非涉及虚拟基础和更深的继承层次结构)。

On the other hand, if the list classes is fixed, but new type-discriminating operations are likely to be introduced (or if there's simply too many of them to maintain), consider the Visitor pattern instead. 另一方面,如果列表类是固定的,但是可能会引入新的区分类型的操作(或者如果要维护的类型太多),请考虑使用Visitor模式 Give your Animal class a virtual visitor-accepting function: 为您的Animal类提供虚拟的访客接受功能:

virtual void accept(AnimalVisitor &v) = 0;

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

Then, each derived class will implement it: 然后,每个派生类将实现它:

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

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

And your operations will just use it: 您的操作将只使用它:

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

If you're going to be adding both new derived classes and new operations, you can add non-abstract functions to the above visitor so that existing code which doesn't need to know about the new classes does not break: 如果要同时添加新的派生类和新的操作,则可以向上述访问者添加非抽象函数,以使不需要了解新类的现有代码不会中断:

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

Your first option will be faster, but only if you fix the erroneous dynamic_cast (it should be static_cast ): 您的第一个选择会更快,但dynamic_cast是您必须修复错误的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();
}

The point of using manual dispatch on get_type() here is that it allows you to avoid the expensive call to __dynamic_cast in the C++ runtime. 在此对get_type()使用手动分派的get_type()在于,它可以避免在C ++运行时中对__dynamic_cast进行昂贵的调用。 As soon as you've made that call into the runtime, you lose. 一旦将该调用发送到运行时,您就会迷失方向。

If you use the final qualifier on both Dog and Cat (that is, on every class in your program that you know will never have child classes), then you will have enough information to know that 如果您在DogCat上都使用了final限定词(也就是说,在程序中每个您知道永远不会有子类的类上),那么将有足够的信息来知道

dynamic_cast<Dog*>(animal)

can be implemented as a simple pointer comparison; 可以实现为简单的指针比较; but sadly (as of 2017) neither GCC nor Clang implements such an optimization. 但令人遗憾的是(截至2017年)GCC和Clang都未实现这种优化。 You can do the optimization by hand, without introducing a get_type method, by using the C++ typeid operator: 您可以使用C ++ typeid运算符手动进行优化,而无需引入get_type方法:

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

Compiling with clang++ -std=c++14 -O3 -S should show you the benefits of the third approach here. 使用clang++ -std=c++14 -O3 -S编译应该会在这里向您展示第三种方法的好处。

action_option1 starts off with 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 improves it to 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

(notice that in the fixed version, the call to __dynamic_cast is gone, replaced by just a little pointer math). (请注意,在固定版本中,对__dynamic_cast的调用已消失,仅由一点指针数学取代了)。

action_option2 is actually shorter than action_option1 because it doesn't add a virtual call on top of the __dynamic_cast , but it's still awful: 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

And here's action_option3 . 这是action_option3 It's small enough that I can just paste the entire function definition here, instead of excerpting: 它足够小,我可以在此处粘贴整个函数定义,而不必摘录:

__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

The __cxa_bad_typeid cruft at the end is because it might be the case that animal == nullptr . 最后使用__cxa_bad_typeid原因是因为animal == nullptr可能是这种情况。 You can eliminate that cruft by making your parameter of type Animal& instead of Animal* , so that the compiler knows it's non-null. 您可以通过将参数设置为Animal&而不是Animal*类型来消除这种麻烦,以便编译器知道它为非null。

I tried adding this line at the top of the function: 我尝试在函数顶部添加以下行:

if (animal == nullptr) __builtin_unreachable();

but sadly, Clang's implementation of typeid didn't pick up on that hint. 但是可悲的是,Clang对typeid的实现没有得到该提示。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM