考虑:
#include<iostream>
using namespace std;
class Base
{
public:
virtual void show() { cout<<" In Base \n"; }
};
class Derived: public Base
{
public:
void show() { cout<<"In Derived \n"; }
};
int main(void)
{
Base *bp = new Derived;
bp->show(); // RUN-TIME POLYMORPHISM
return 0;
}
为什么这段代码导致运行时多态,为什么不能在编译时解决它?
答案 0 :(得分:111)
因为在一般情况下,它在编译时不可能来确定它在运行时的类型。您的示例可以在编译时解决(请参阅@Quentin的回答),但可以构建无法解决的案例,例如:
Base *bp;
if (rand() % 10 < 5)
bp = new Derived;
else
bp = new Base;
bp->show(); // only known at run time
编辑:感谢@nwp,这是一个更好的案例。类似的东西:
Base *bp;
char c;
std::cin >> c;
if (c == 'd')
bp = new Derived;
else
bp = new Base;
bp->show(); // only known at run time
另外,根据Turing's proof的推论,可以证明在一般情况下它在数学上不可能让C ++编译器知道基类指针指向什么在运行时。
假设我们有类似C ++编译器的函数:
bool bp_points_to_base(const string& program_file);
其输入为program_file
:任何 C ++源代码文本文件的名称,其中指针bp
(如在OP中)调用其{{1成员函数virtual
。并且可以在一般情况下确定 (在序列点show()
,其中A
成员函数virtual
首先通过show()
调用):是否指针bp
指向bp
的实例。
考虑以下C ++程序片段&#34; q.cpp&#34;:
Base
现在,如果Base *bp;
if (bp_points_to_base("q.cpp")) // invokes bp_points_to_base on itself
bp = new Derived;
else
bp = new Base;
bp->show(); // sequence point A
确定&#34; q.cpp&#34;:bp_points_to_base
指向bp
Base
的实例,那么&#34; q.cpp&#34;将A
指向bp
处的其他内容。如果它确定&#34; q.cpp&#34;:A
并未指向bp
Base
的实例,那么&#34; q的.cpp&#34;将A
指向bp
Base
的实例。这是一个矛盾。所以我们最初的假设不正确。因此,A
无法为一般情况撰写。
答案 1 :(得分:80)
当已知对象的静态类型时,编译器会定期对此类调用进行虚拟化。将代码按原样粘贴到Compiler Explorer会生成以下程序集:
main: # @main
pushq %rax
movl std::cout, %edi
movl $.L.str, %esi
movl $12, %edx
callq std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
xorl %eax, %eax
popq %rdx
retq
pushq %rax
movl std::__ioinit, %edi
callq std::ios_base::Init::Init()
movl std::ios_base::Init::~Init(), %edi
movl std::__ioinit, %esi
movl $__dso_handle, %edx
popq %rax
jmp __cxa_atexit # TAILCALL
.L.str:
.asciz "In Derived \n"
即使您无法阅读程序集,也可以看到可执行文件中只有"In Derived \n"
。动态调度不仅已经过优化,整个基类也是如此。
答案 2 :(得分:30)
为什么这段代码导致运行时多态,为什么不能在编译时解决?
是什么让你认为它呢?
您正在做出一个共同的假设:仅仅因为语言标识了这种情况,因为使用运行时多态并不意味着实现在运行时保持分派。 C ++标准有一个所谓的“as-if”规则:C ++标准规则的可观察效果是关于抽象机器描述的,并且实现可以自由地实现所述可观察的效果,但是它们是愿望。
实际上, devirtualization 是用于谈论编译器优化的一般词,旨在解决在编译时对虚拟方法的调用。
目标不是削减几乎无法察觉的虚拟呼叫开销(如果分支预测效果很好),而是关于移除黑盒子。在优化方面,最好的收益是内联调用的关键:这会打开常量传播和大量优化,并且只有在被调用函数的主体是在编译时已知(因为它涉及删除调用并由函数体替换它)。
一些虚拟化机会:
final
方法或virtual
类的final
方法的调用非常简单化virtual
方法的调用可以被虚拟化virtual
方法(如果您的示例是这种情况,则构造属于同一函数) )但是,对于最先进的技术,您将需要阅读HonzaHubička的博客。 Honza是一名gcc开发人员,去年他参与了推测性虚拟化:目标是计算动态类型为A,B或C的概率,然后推测性地将调用虚拟化,就像转换一样: / p>
Base& b = ...;
b.call();
成:
Base& b = ...;
if (b.vptr == &VTableOfA) { static_cast<A&>(b).call(); }
else if (b.vptr == &VTableOfB) { static_cast<B&>(b).call(); }
else if (b.vptr == &VTableOfC) { static_cast<C&>(b).call(); }
else { b.call(); } // virtual call as last resort
Honza做了一个由5部分组成的帖子:
答案 3 :(得分:13)
编译器通常无法用静态调用替换运行时决策的原因有很多,主要是因为它涉及编译时不可用的信息,例如:配置或用户输入。除此之外,我想指出为什么一般不可能这样做的另外两个原因。
首先,C ++编译模型基于单独的编译单元。编译一个单元时,编译器只知道正在编译的源文件中定义的内容。考虑一个带有基类的编译单元和一个引用基类的函数:
struct Base {
virtual void polymorphic() = 0;
};
void foo(Base& b) {b.polymorphic();}
单独编译时,编译器不了解实现Base
的类型,因此无法删除动态调度。它也不是我们想要的东西,因为我们希望能够通过实现接口来扩展具有新功能的程序。可以在链接时执行此操作,但仅在假设程序完全完成的情况下执行此操作。动态库可以打破这种假设,如下所示,总会出现根本无法完成的情况。
更根本的原因来自可计算性理论。即使有完整的信息,也无法定义一种算法来计算是否达到程序中的某一行。如果可以,您可以解决停机问题:对于程序P
,我通过在P'
末尾添加一行来创建新程序P
。该算法现在能够决定是否到达该行,这解决了停机问题。
一般无法决定意味着编译器无法决定将哪个值分配给变量,例如。
bool someFunction( /* arbitrary parameters */ ) {
// ...
}
// ...
Base* b = nullptr;
if (someFunction( ... ))
b = new Derived1();
else
b = new Derived2();
b->polymorphicFunction();
即使在编译时已知所有参数,也无法证明通常会采用程序的哪条路径以及哪种静态类型b
。可以通过优化编译器来实现近似,但总会出现无法正常工作的情况。
话虽如此,C ++编译器非常努力地去除动态调度,因为它打开了许多其他优化机会,主要是因为能够通过代码内联和传播知识。如果您感兴趣,you can find an interesting serious of blog posts for the GCC虚拟化实现。
答案 4 :(得分:12)
如果优化器选择这样做,那么在编译时可以很容易地解决这个问题。
该标准指定了与运行时多态性发生时相同的行为。它并不具体通过实际运行时多态性来实现。
答案 5 :(得分:1)
基本上,编译器应该能够确定在非常简单的情况下这不会导致运行时多态性。很可能有编译器实际上这样做,但这主要是一个猜想。
有问题的是一般案例,当你实际构建一个复杂的,并且具有库依赖性的一部分案例,或者分析编译后多个编译单元的复杂性时,这将需要保留相同代码的多个版本,这将是吹出 AST生成,真正的问题归结为可判定性和停止问题。
如果可以通话,后者不允许解决问题 在一般情况下是虚拟的。
暂停问题是判断给定输入的程序是否会停止(我们说程序输入对停止)。众所周知,没有通用算法,例如, 编译器,它解决了所有可能的程序输入对。
为了让编译器决定是否应该调用 virtual ,任何程序,它应该能够决定所有可能的程序输入对。
为了做到这一点,编译器需要有一个算法A来决定给定的程序P1和程序P2,其中P2进行虚拟调用,然后编程P3 {while({P1,I}!= {P2,I }}}暂停任何输入我。
因此,能够找出所有可能的虚拟化的编译器应该能够确定对于所有可能的P3和I的任何对(P3,I);这对于所有可能是不可判定的,因为A不存在。但是,可以根据具体情况决定是否可以进行眼球眩目。
这就是为什么在您的情况下,呼叫可以被虚拟化,但不是任何 情况下。