42

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%を取得するには、コードで何をする必要がありますか?

  • g ++(Ubuntu / Linaro 4.5.2-8ubuntu4)4.5.2
  • gcov(Ubuntu / Linaro 4.5.2-8ubuntu4)4.5.2
4

3 に答える 3

61

通常の実装では、デストラクタには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, %eaxmovl $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で生成されたコードには、このような意味のないアクセスできないブランチがたくさんあります。

于 2011-08-26T03:24:11.513 に答える
7

デストラクタで、GCC は決して true にならない条件の条件ジャンプを生成しました (1 が割り当てられただけなので、%al はゼロではありません)。

[...]
  29:   b8 01 00 00 00          mov    $0x1,%eax
  2e:   84 c0                   test   %al,%al
  30:   74 30                   je     62 <_ZN3FooD0Ev+0x62>
[...]
于 2011-08-26T03:15:01.797 に答える