通常の実装では、デストラクタには2つのブランチがあります。1つは非動的オブジェクトの破棄用で、もう1つは動的オブジェクトの破棄用です。特定のブランチの選択は、呼び出し元によってデストラクタに渡された非表示のブールパラメータを介して実行されます。通常、0または1としてレジスタを通過します。
あなたの場合、破壊は非動的オブジェクトに対するものであるため、動的分岐は行われないと思います。new
クラスの-ed、次にdelete
-edオブジェクトを追加してみてください。そうFoo
すれば、2番目のブランチも取得されるはずです。
この分岐が必要な理由は、C++言語の仕様に基づいています。一部のクラスが独自のを定義する場合、呼び出すoperator delete
特定のクラスの選択はoperator delete
、クラスデストラクタ内から検索されたかのように行われます。その最終結果は、仮想デストラクタを備えたクラスのoperator delete
場合、それが仮想関数であるかのように動作することです(正式にはクラスの静的メンバーであるにもかかわらず)。
多くのコンパイラは、この動作を文字通り実装します。適切なoperator delete
ものは、デストラクタ実装内から直接呼び出されます。もちろん、動的にoperator delete
割り当てられたオブジェクトを破棄する場合にのみ呼び出す必要があります(ローカルオブジェクトや静的オブジェクトの場合は呼び出さないでください)。これを実現するために、への呼び出しは、上記の非表示パラメーターによって制御されるブランチに配置されます。operator delete
あなたの例では、物事はかなり些細なことに見えます。オプティマイザーが不要な分岐をすべて削除することを期待します。しかし、どういうわけかそれは最適化を乗り切ることができたようです。
ここに少し追加の調査があります。このコードを検討してください
#include <stdio.h>
struct A {
void operator delete(void *) { scanf("11"); }
virtual ~A() { printf("22"); }
};
struct B : A {
void operator delete(void *) { scanf("33"); }
virtual ~B() { printf("44"); }
};
int main() {
A *a = new B;
delete a;
}
A
これは、デフォルトの最適化設定でGCC4.3.4を使用するコンパイラーの場合のデストラクタのコードがどのようになるかを示しています。
__ZN1AD2Ev: ; destructor A::~A
LFB8:
pushl %ebp
LCFI8:
movl %esp, %ebp
LCFI9:
subl $8, %esp
LCFI10:
movl 8(%ebp), %eax
movl $__ZTV1A+8, (%eax)
movl $LC1, (%esp) ; LC1 is "22"
call _printf
movl $0, %eax ; <------ Note this
testb %al, %al ; <------
je L10 ; <------
movl 8(%ebp), %eax ; <------
movl %eax, (%esp) ; <------
call __ZN1AdlEPv ; <------ calling `A::operator delete`
L10:
leave
ret
(のデストラクタはB
もう少し複雑なので、A
ここで例として使用します。ただし、問題の分岐に関する限り、のデストラクタはB
同じ方法でそれを行います)。
ただし、このデストラクタの直後に、生成されたコードには、命令が命令に置き換えられていることを除いて、まったく同じように見える、まったく同じクラスのデストラクタの別のバージョンがA
含まれています。movl $0, %eax
movl $1, %eax
__ZN1AD0Ev: ; another destructor A::~A
LFB10:
pushl %ebp
LCFI13:
movl %esp, %ebp
LCFI14:
subl $8, %esp
LCFI15:
movl 8(%ebp), %eax
movl $__ZTV1A+8, (%eax)
movl $LC1, (%esp) ; LC1 is "22"
call _printf
movl $1, %eax ; <------ See the difference?
testb %al, %al ; <------
je L14 ; <------
movl 8(%ebp), %eax ; <------
movl %eax, (%esp) ; <------
call __ZN1AdlEPv ; <------ calling `A::operator delete`
L14:
leave
ret
矢印でラベル付けしたコードブロックに注意してください。これはまさに私が話していたものです。レジスタal
はその隠しパラメータとして機能します。operator delete
この「疑似ブランチ」は、の値に従って、への呼び出しを呼び出すかスキップすることになっていますal
。ただし、デストラクタの最初のバージョンでは、このパラメータはいつものように本体にハードコーディングされていますが0
、2番目のバージョンではいつものようにハードコーディングされてい1
ます。
クラスB
には、そのために生成された2つのバージョンのデストラクタもあります。したがって、コンパイルされたプログラムには4つの特徴的なデストラクタがあります。クラスごとに2つのデストラクタです。
当初、コンパイラーは単一の「パラメーター化された」デストラクタ(上記のブレークとまったく同じように機能します)の観点から内部的に考えていたと推測できます。次に、パラメーター化されたデストラクタを2つの独立した非パラメーター化バージョンに分割することを決定しました。1つは0
(非動的デストラクタ)のハードコードされたパラメーター値用で、もう1つは1
(動的デストラクタ)のハードコードされたパラメーター値用です。非最適化モードでは、関数の本体内に実際のパラメーター値を割り当て、すべての分岐を完全にそのままにして、文字通りそれを行います。これは、最適化されていないコードでは許容できると思います。そしてそれはまさにあなたが扱っていることです。
言い換えれば、あなたの質問に対する答えは次のとおりです。この場合、コンパイラにすべての分岐を実行させることは不可能です。100%のカバレッジを達成する方法はありません。これらのブランチのいくつかは「死んでいます」。このバージョンのGCCでは、最適化されていないコードを生成するためのアプローチがかなり「怠惰」で「緩い」というだけです。
最適化されていないモードでの分割を防ぐ方法があるかもしれないと思います。まだ見つけていません。または、おそらく、それはできません。古いバージョンのGCCは、真のパラメーター化されたデストラクタを使用していました。たぶん、このバージョンのGCCでは、2つのデストラクタのアプローチに切り替えることを決定し、その間、オプティマイザが役に立たないブランチをクリーンアップすることを期待して、既存のコードジェネレータをそのような迅速で汚い方法で「再利用」しました。
最適化を有効にしてコンパイルしている場合、GCCは、最終的なコードでの無用な分岐などの贅沢を許可しません。おそらく、最適化されたコードの分析を試みる必要があります。最適化されていないGCCで生成されたコードには、このような意味のないアクセスできないブランチがたくさんあります。