为什么不能在编译时解决运行时多态?

时间:2015-12-18 11:22:09

标签: c++ polymorphism

考虑:

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

为什么这段代码导致运行时多态,为什么不能在编译时解决它?

6 个答案:

答案 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不存在。但是,可以根据具体情况决定是否可以进行眼球眩目。

  

这就是为什么在您的情况下,呼叫可以被虚拟化,但不是任何   情况下。