web-dev-qa-db-ja.com

コンパイル時にランタイムポリモーフィズムを解決できないのはなぜですか?

考慮してください:

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

なぜこのコードはランタイムポリモーフィズムを引き起こし、コンパイル時に解決できないのですか?

73
Hasnat

一般的な場合、実行時にどのタイプになるかを決定するのは、コンパイル時にimpossibleであるためです。サンプルはコンパイル時に解決できます(@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 
_

また、 チューリングの証明 の帰結により、一般的な場合であることが示されますC++コンパイラーが基本クラスポインターを知ることは数学的に不可能です実行時を指します。

C++コンパイラのような関数があると仮定します。

_bool bp_points_to_base(const string& program_file);
_

これは、入力として_program_file_を取ります:anyポインターbp(OPのように)がそのvirtualメンバー関数show()。また、一般的な場合にを決定できます(シーケンス変数Aメンバー関数show()virtualを介して最初に呼び出されるシーケンスポイントbp):ポインターbpBaseのインスタンスかどうか。

C++プログラム「q.cpp」の次のフラグメントを検討してください。

_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
_

ここで_bp_points_to_base_が "q.cpp"で以下を決定した場合:bpBaseAのインスタンスをポイントし、次に "q.cpp"はbpAの別の何かをポイントします。また、「q.cpp」でbpBaseAのインスタンスを指していないと判断した場合、「q.cpp」はbpBaseのインスタンスをAのインスタンスに向けます。これは矛盾です。したがって、最初の仮定は間違っています。したがって、_bp_points_to_base_は、一般的な場合には記述できません。

112
Paul Evans

コンパイラは、オブジェクトの静的型がわかっている場合、そのような呼び出しを日常的に仮想化します。コードをそのまま 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"は実行可能ファイルに存在します。動的ディスパッチが最適化されているだけでなく、基本クラス全体も最適化されています。

80
Quentin

なぜこのコードはランタイムポリモーフィズムを引き起こし、コンパイル時に解決できないのですか?

何があなたをそれがそう思うと思わせますか?

あなたは一般的な仮定を立てています:languageが実行時ポリモーフィズムを使用することでこのケースを識別するからといって、implementationは、実行時にディスパッチされます。 C++標準には、いわゆる「as-if」ルールがあります。C++標準ルールの観測可能な効果は、抽象マシンに関して説明されています。また、実装は、観察可能な効果を自由に達成できます。


実際、devirtualizationは、コンパイル時の仮想メソッドへの呼び出しを解決することを目的としたコンパイラの最適化について話すために使用される一般的なWordです。

目標は、ほとんど気付かないほどの仮想呼び出しのオーバーヘッドを削減することではなく(分岐予測がうまく機能する場合)、ブラックボックスを削除することです。最適化に関しては、inlining呼び出しに最適なゲインを設定します。これにより、一定の伝播と最適化の多くが開かれ、インライン化のみが可能になります。呼び出される関数の本体がコンパイル時にわかっている場合に達成されます(呼び出しを削除し、それを関数の本体に置き換える必要があるため)。

いくつかの仮想化の機会:

  • finalメソッドの呼び出しまたはvirtualクラスのfinalメソッドの呼び出しは簡単に仮想化されます
  • 匿名の名前空間で定義されたクラスのvirtualメソッドの呼び出しは、そのクラスが階層のリーフである場合、仮想化されない場合があります
  • コンパイル時にオブジェクトの動的な型を確立できる場合、基本クラスを介したvirtualメソッドの呼び出しは仮想化される場合があります(これは、同じ関数内にある構造の例です)

ただし、最新技術については、HonzaHubičkaのブログをご覧ください。 Honzaはgcc開発者であり、昨年彼は投機的仮想化に取り組みました:目標は、A、B、またはCのいずれかの動的タイプの確率を計算することですそして、変換をやや変換するような呼び出しを投機的に仮想化します。

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部構成の投稿を行いました。

30
Matthieu M.

オプティマイザーがそうすることを選択した場合、コンパイル時に簡単に解決できます。

この規格では、実行時多型が発生した場合と同じ動作を指定しています。実際の実行時ポリモーフィズムによって達成されることは明確ではありません。

12
JSF

基本的に、コンパイラは、これが非常に単純なケースで実行時ポリモーフィズムにならないことを理解できるはずです。ほとんどの場合、実際にそれを行うコンパイラがありますが、それはほとんど推測です。

問題は、実際に複雑なライブラリを構築している一般的なケースであり、ライブラリの依存関係や、同じコードの複数のバージョンを保持する必要があるコンパイル後の複数のコンパイル単位の分析の複雑さを伴うケースの一部です- AST生成、実際の問題は決定可能性と停止する問題に要約されます。

後者では、一般的なケースでコールを仮想化できる場合、問題を解決することはできません。

haltingの問題は、入力が与えられたプログラムが停止するかどうかを決定することです(、プログラムと入力のペアが停止する)。一般的なアルゴリズムはないことが知られています。 コンパイラ、考えられるすべてのプログラムと入力のペアを解決します。

コンパイラーがvirtual呼び出しを行う必要があるかどうかをプログラムで決定するために、すべての可能なプログラムでそれを決定できる必要があります-入力ペア。

そのためには、コンパイラーは、P2が仮想呼び出しを行う特定のプログラムP1およびプログラムP2を決定するアルゴリズムAを持っている必要があります。その後、プログラムP3 {while({P1、I}!= {P2、I})}入力Iに対して停止します。

したがって、コンパイラは、すべての可能な仮想化を把握できるため、すべての可能なP3とIの任意のペア(P3、I)を決定できるはずです。これは、Aが存在しないため、すべてに対して決定不能です。ただし、目を光らせることができる特定のケースについて決定することができます。

そのため、あなたの場合、呼び出しは仮想化できますが、どのような場合でもできません。

1
g24l