为 class 实施标识符或使用 dynamic_cast
Implement an identifier for a class or use dynamic_cast
我的问题与 What's the point of IsA() in C++? 有关。我有一个性能关键代码,其中包含在特定位置处理来自派生 classes 的特定函数,其中只有基指针可用。检查我们拥有哪个派生 class 的最佳方法是什么?我编写了两个选项,在第二个选项中我可以消除 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;
}
我想引用您所引用问题的公认答案:
In modern C++ there is no point.
对于您的示例,最简单的解决方案是使用动态调度:
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;
}
对于更复杂的场景,可以考虑Strategy、Template Method或Visitor等设计模式。
如果这确实是性能瓶颈,将 Dog
和 Cat
声明为 final
可能会有所帮助。
这在很大程度上取决于您的性能关键代码的性能关键程度。我见过这样的设置,即使是虚拟函数的动态调度也太昂贵了,所以如果你在这样的领域,忘记 dynamic_cast
并手工制作一些东西。
不过,我假设您可以进行一两次虚拟通话。您可能希望避开 dynamic_cast
,因为它通常比动态调度慢得多。
现在,您有 N classes 来自公共基础和 M 代码中您需要的点根据得出的具体结果做出决定class。问题是:N,M中哪个更容易在未来发生变化?您是否更有可能添加新的派生 classes,或引入类型决策重要的新点?此答案将决定最适合您的设计。
如果您要添加新的 classes,但类型区分位置的数量是固定的(理想情况下也很小),枚举方法将是最佳选择。只需使用 static_cast
而不是 dynamic_cast
;如果您知道实际的运行时类型,则不需要访问 RTTI 来为您进行转换(除非涉及虚拟基和更深的继承层次结构)。
另一方面,如果列表 classes 是固定的,但可能会引入新的类型区分操作(或者如果它们太多而无法维护),请考虑 Visitor pattern 代替。给你的 Animal
class 一个虚拟的访客接受功能:
virtual void accept(AnimalVisitor &v) = 0;
struct AnimalVisitor
{
virtual void visit(Dog &dog) = 0;
virtual void visit(Cat &cat) = 0;
};
然后,每个派生的class将实现它:
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);
}
如果您要添加新的派生 classes 和新操作,您可以向上述访问者添加非抽象函数,这样现有代码就不需要知道新的 classes 不会中断:
struct AnimalVisitor
{
virtual void visit(Dog &d) = 0;
virtual void visit(Cat &c) = 0;
virtual void visit(Parrot &p) {}
};
您的第一个选项会更快,但前提是您修复了错误的 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
的昂贵调用。一旦您对运行时进行了调用,您就输了。
如果您在 Dog
和 Cat
上都使用 final
限定符(也就是说,在您知道永远不会有子 class 的程序中的每个 class =72=]es),那么你将有足够的信息知道
dynamic_cast<Dog*>(animal)
可以实现为简单的指针比较;但遗憾的是(截至 2017 年)GCC 和 Clang 都没有实现这样的优化。您可以通过使用 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();
}
使用 clang++ -std=c++14 -O3 -S
编译应该向您展示第三种方法的好处。
action_option1
从
开始
movq %rdi, %rbx
movq (%rbx), %rax
callq *(%rax)
cmpl , %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 , %rsp
popq %rbx
popq %rbp
jmp __ZNK3Dog11go_for_walkEv ## TAILCALL
action_option1_fixed
改进为
movq %rdi, %rbx
movq (%rbx), %rax
callq *(%rax)
cmpl , %eax
jne LBB2_1
movq %rbx, %rdi
addq , %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 , %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
实现没有接受该提示。
我的问题与 What's the point of IsA() in C++? 有关。我有一个性能关键代码,其中包含在特定位置处理来自派生 classes 的特定函数,其中只有基指针可用。检查我们拥有哪个派生 class 的最佳方法是什么?我编写了两个选项,在第二个选项中我可以消除 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;
}
我想引用您所引用问题的公认答案:
In modern C++ there is no point.
对于您的示例,最简单的解决方案是使用动态调度:
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;
}
对于更复杂的场景,可以考虑Strategy、Template Method或Visitor等设计模式。
如果这确实是性能瓶颈,将 Dog
和 Cat
声明为 final
可能会有所帮助。
这在很大程度上取决于您的性能关键代码的性能关键程度。我见过这样的设置,即使是虚拟函数的动态调度也太昂贵了,所以如果你在这样的领域,忘记 dynamic_cast
并手工制作一些东西。
不过,我假设您可以进行一两次虚拟通话。您可能希望避开 dynamic_cast
,因为它通常比动态调度慢得多。
现在,您有 N classes 来自公共基础和 M 代码中您需要的点根据得出的具体结果做出决定class。问题是:N,M中哪个更容易在未来发生变化?您是否更有可能添加新的派生 classes,或引入类型决策重要的新点?此答案将决定最适合您的设计。
如果您要添加新的 classes,但类型区分位置的数量是固定的(理想情况下也很小),枚举方法将是最佳选择。只需使用 static_cast
而不是 dynamic_cast
;如果您知道实际的运行时类型,则不需要访问 RTTI 来为您进行转换(除非涉及虚拟基和更深的继承层次结构)。
另一方面,如果列表 classes 是固定的,但可能会引入新的类型区分操作(或者如果它们太多而无法维护),请考虑 Visitor pattern 代替。给你的 Animal
class 一个虚拟的访客接受功能:
virtual void accept(AnimalVisitor &v) = 0;
struct AnimalVisitor
{
virtual void visit(Dog &dog) = 0;
virtual void visit(Cat &cat) = 0;
};
然后,每个派生的class将实现它:
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);
}
如果您要添加新的派生 classes 和新操作,您可以向上述访问者添加非抽象函数,这样现有代码就不需要知道新的 classes 不会中断:
struct AnimalVisitor
{
virtual void visit(Dog &d) = 0;
virtual void visit(Cat &c) = 0;
virtual void visit(Parrot &p) {}
};
您的第一个选项会更快,但前提是您修复了错误的 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
的昂贵调用。一旦您对运行时进行了调用,您就输了。
如果您在 Dog
和 Cat
上都使用 final
限定符(也就是说,在您知道永远不会有子 class 的程序中的每个 class =72=]es),那么你将有足够的信息知道
dynamic_cast<Dog*>(animal)
可以实现为简单的指针比较;但遗憾的是(截至 2017 年)GCC 和 Clang 都没有实现这样的优化。您可以通过使用 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();
}
使用 clang++ -std=c++14 -O3 -S
编译应该向您展示第三种方法的好处。
action_option1
从
movq %rdi, %rbx
movq (%rbx), %rax
callq *(%rax)
cmpl , %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 , %rsp
popq %rbx
popq %rbp
jmp __ZNK3Dog11go_for_walkEv ## TAILCALL
action_option1_fixed
改进为
movq %rdi, %rbx
movq (%rbx), %rax
callq *(%rax)
cmpl , %eax
jne LBB2_1
movq %rbx, %rdi
addq , %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 , %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
实现没有接受该提示。