Gcovを使用してC++コードのテストカバレッジを測定すると、デストラクタのブランチが報告されます。
struct Foo
{
virtual ~Foo()
{
}
};
int main (int argc, char* argv[])
{
Foo f;
}
分岐確率を有効にしてgcovを実行すると(-b)、次の出力が得られます。
$ gcov /home/epronk/src/lcov-1.9/example/example.gcda -o /home/epronk/src/lcov-1.9/example -b
File 'example.cpp'
Lines executed:100.00% of 6
Branches executed:100.00% of 2
Taken at least once:50.00% of 2
Calls executed:40.00% of 5
example.cpp:creating 'example.cpp.gcov'
気になるのは「1回以上撮った:2の50.00%」。
生成された.gcovファイルに詳細が記載されています。
$ cat example.cpp.gcov | c++filt
-: 0:Source:example.cpp
-: 0:Graph:/home/epronk/src/lcov-1.9/example/example.gcno
-: 0:Data:/home/epronk/src/lcov-1.9/example/example.gcda
-: 0:Runs:1
-: 0:Programs:1
-: 1:struct Foo
function Foo::Foo() called 1 returned 100% blocks executed 100%
1: 2:{
function Foo::~Foo() called 1 returned 100% blocks executed 75%
function Foo::~Foo() called 0 returned 0% blocks executed 0%
1: 3: virtual ~Foo()
1: 4: {
1: 5: }
branch 0 taken 0% (fallthrough)
branch 1 taken 100%
call 2 never executed
call 3 never executed
call 4 never executed
-: 6:};
-: 7:
function main called 1 returned 100% blocks executed 100%
1: 8:int main (int argc, char* argv[])
-: 9:{
1: 10: Foo f;
call 0 returned 100%
call 1 returned 100%
-: 11:}
「ブランチ0が0%(フォールスルー)を取得」という行に注意してください。
このブランチの原因と、ここで100%を取得するには、コードで何をする必要がありますか?
通常の実装では、デストラクタには通常2つのブランチがあります。1つは非動的オブジェクト破壊用、もう1つは動的オブジェクト破壊用です。特定のブランチの選択は、呼び出し元によってデストラクタに渡される非表示のブールパラメータを介して実行されます。通常、0または1としてレジスタを通過します。
あなたの場合、破壊は非動的オブジェクトに対するものであるため、動的分岐は行われないと思います。クラスnew
のdelete
- ed、次にFoo
- edオブジェクトを追加してみてください。そうすれば、2番目のブランチも取得されるはずです。
この分岐が必要な理由は、C++言語の仕様に基づいています。一部のクラスが独自のoperator delete
を定義する場合、呼び出す特定のoperator delete
の選択は、クラスデストラクタ内から検索されたかのように行われます。その結果、仮想デストラクタoperator delete
を持つクラスは、virtual関数であるかのように動作します(正式にはstaticクラスのメンバー)。
多くのコンパイラはこの動作を実装します文字通り:適切な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;
}
これは、デフォルトの最適化設定でGCC 4.3.4を使用するコンパイラーの場合、A
のデストラクタのコードがどのようになるかを示しています。
__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
は、その隠しパラメータとして機能します。この「疑似ブランチ」は、al
の値に従って、operator delete
への呼び出しを呼び出すかスキップすることになっています。ただし、デストラクタの最初のバージョンでは、このパラメータは常に0
として本体にハードコードされていますが、2番目のバージョンでは常に1
としてハードコードされています。
クラスB
には、そのために生成された2つのバージョンのデストラクタもあります。したがって、コンパイルされたプログラムには4つの特徴的なデストラクタがあります。クラスごとに2つのデストラクタです。
当初、コンパイラーは、単一の「パラメーター化された」デストラクタ(ブレークの上で説明したとおりに機能する)の観点から内部的に考えていたと推測できます。次に、パラメータ化されたデストラクタを2つの独立したパラメータ化されていないバージョンに分割することを決定しました。1つはハードコードされたパラメータ値0
(非動的デストラクタ)用で、もう1つはハードコードされたパラメータ値1
(動的デストラクタ)。非最適化モードでは、関数の本体内に実際のパラメーター値を割り当て、すべての分岐を完全にそのままにして、文字通りそれを行います。これは、最適化されていないコードでは許容できると思います。そしてそれはまさにあなたが扱っているものです。
言い換えれば、あなたの質問に対する答えは次のとおりです。この場合、コンパイラにすべてのブランチを取得させることは不可能です。100%のカバレッジを達成する方法はありません。これらのブランチの一部は「デッド」です。このバージョンのGCCでは、最適化されていないコードを生成するアプローチがかなり「怠惰」で「緩い」というだけです。
最適化されていないモードでの分割を防ぐ方法があるかもしれないと思います。まだ見つけていません。または、おそらく、それはできません。古いバージョンのGCCは、真のパラメーター化されたデストラクタを使用していました。たぶん、このバージョンのGCCでは、2つのデストラクタのアプローチに切り替えることを決定し、その間、オプティマイザが役に立たないブランチをクリーンアップすることを期待して、既存のコードジェネレータをそのような迅速で汚い方法で「再利用」しました。
最適化を有効にしてコンパイルしている場合、GCCは、最終的なコードでの無駄な分岐などの贅沢を許可しません。おそらく、最適化されたコードの分析を試みる必要があります。最適化されていないGCCで生成されたコードには、このような意味のないアクセスできないブランチがたくさんあります。
デストラクタで、GCCは、真になることのない条件に対して条件付きジャンプを生成しました(%alは1が割り当てられただけなので、ゼロではありません)。
[...]
29: b8 01 00 00 00 mov $0x1,%eax
2e: 84 c0 test %al,%al
30: 74 30 je 62 <_ZN3FooD0Ev+0x62>
[...]
Gccバージョン5.4.0でもデストラクタの問題はありますが、Clangでは存在しないようです。
テスト済み:
clang version 3.8.0-2ubuntu4 (tags/RELEASE_380/final)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
次に、「llvm-cov gcov ...」を使用して、説明されているようにカバレッジを生成します ここ 。