考慮してください:
#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;
}
なぜこのコードはランタイムポリモーフィズムを引き起こし、コンパイル時に解決できないのですか?
一般的な場合、実行時にどのタイプになるかを決定するのは、コンパイル時に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
):ポインターbp
がBase
のインスタンスかどうか。
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"で以下を決定した場合:bp
はBase
のA
のインスタンスをポイントし、次に "q.cpp"はbp
をA
の別の何かをポイントします。また、「q.cpp」でbp
がBase
のA
のインスタンスを指していないと判断した場合、「q.cpp」はbp
のBase
のインスタンスをA
のインスタンスに向けます。これは矛盾です。したがって、最初の仮定は間違っています。したがって、_bp_points_to_base
_は、一般的な場合には記述できません。
コンパイラは、オブジェクトの静的型がわかっている場合、そのような呼び出しを日常的に仮想化します。コードをそのまま 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"
は実行可能ファイルに存在します。動的ディスパッチが最適化されているだけでなく、基本クラス全体も最適化されています。
なぜこのコードはランタイムポリモーフィズムを引き起こし、コンパイル時に解決できないのですか?
何があなたをそれがそう思うと思わせますか?
あなたは一般的な仮定を立てています: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部構成の投稿を行いました。
オプティマイザーがそうすることを選択した場合、コンパイル時に簡単に解決できます。
この規格では、実行時多型が発生した場合と同じ動作を指定しています。実際の実行時ポリモーフィズムによって達成されることは明確ではありません。
基本的に、コンパイラは、これが非常に単純なケースで実行時ポリモーフィズムにならないことを理解できるはずです。ほとんどの場合、実際にそれを行うコンパイラがありますが、それはほとんど推測です。
問題は、実際に複雑なライブラリを構築している一般的なケースであり、ライブラリの依存関係や、同じコードの複数のバージョンを保持する必要があるコンパイル後の複数のコンパイル単位の分析の複雑さを伴うケースの一部です- AST生成、実際の問題は決定可能性と停止する問題に要約されます。
後者では、一般的なケースでコールを仮想化できる場合、問題を解決することはできません。
haltingの問題は、入力が与えられたプログラムが停止するかどうかを決定することです(、プログラムと入力のペアが停止する)。一般的なアルゴリズムはないことが知られています。 コンパイラ、考えられるすべてのプログラムと入力のペアを解決します。
コンパイラーがvirtual呼び出しを行う必要があるかどうかをプログラムで決定するために、すべての可能なプログラムでそれを決定できる必要があります-入力ペア。
そのためには、コンパイラーは、P2が仮想呼び出しを行う特定のプログラムP1およびプログラムP2を決定するアルゴリズムAを持っている必要があります。その後、プログラムP3 {while({P1、I}!= {P2、I})}入力Iに対して停止します。
したがって、コンパイラは、すべての可能な仮想化を把握できるため、すべての可能なP3とIの任意のペア(P3、I)を決定できるはずです。これは、Aが存在しないため、すべてに対して決定不能です。ただし、目を光らせることができる特定のケースについて決定することができます。
そのため、あなたの場合、呼び出しは仮想化できますが、どのような場合でもできません。