当我使用 gcov 来测量 C++ 代码的测试覆盖率时,它会报告析构函数中的分支。
struct Foo
{
virtual ~Foo()
{
}
};
int main (int argc, char* argv[])
{
Foo f;
}
当我在启用分支概率 (-b) 的情况下运行 gcov 时,我得到以下输出。
$ 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'
困扰我的部分是“至少服用一次: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:}
注意“branch 0 take 0% (fallthrough)”这一行。
是什么导致了这个分支,我需要在代码中做什么才能在这里得到 100%?
最佳答案
在典型的实现中,析构函数通常有两个分支:一个用于非动态对象销毁,另一个用于动态对象销毁。特定分支的选择是通过调用者传递给析构函数的隐藏 bool 参数来执行的。它通常作为 0 或 1 通过寄存器传递。
我猜想,因为在你的情况下,破坏是针对非动态对象的,所以不采用动态分支。尝试添加 new
-ed 然后 delete
-ed 类的对象 Foo
并且第二个分支也应该被采用。
这种分支的必要性源于 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
的析构函数的代码在默认优化设置下使用 GCC 4.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
,而在第二个中,它一如既往地硬编码 1
.类(class)
B
还为它生成了两个版本的析构函数。所以我们最终在编译的程序中有 4 个不同的析构函数:每个类有两个析构函数。我可以猜测,一开始编译器在内部考虑了单个“参数化”析构函数(它的工作原理与我上面描述的中断完全相同)。然后决定将参数化析构函数拆分为两个独立的非参数化版本:一个用于硬编码的参数值
0
(非动态析构函数)和另一个用于 1
的硬编码参数值(动态析构函数)。在非优化模式下,它通过在函数体内分配实际参数值并保持所有分支完全完好无损来做到这一点。我猜这在非优化代码中是可以接受的。而这正是你正在处理的。换句话说,你的问题的答案是:在这种情况下,不可能让编译器采用所有分支。没有办法达到 100% 的覆盖率。其中一些分支是“死的”。 只是在这个版本的 GCC 中,生成非优化代码的方法比较“懒惰”和“松散”。
我认为可能有一种方法可以防止非优化模式下的拆分。只是我还没找到。或者,很可能,它无法完成。旧版本的 GCC 使用真正的参数化析构函数。也许在这个版本的 GCC 中,他们决定切换到两个析构函数的方法,同时他们以如此快速和肮脏的方式“重用”现有的代码生成器,期望优化器清除无用的分支。
当您在启用优化的情况下进行编译时,GCC 将不允许自己在最终代码中使用无用的分支等奢侈品。您可能应该尝试分析优化的代码。非优化 GCC 生成的代码有很多像这样的无意义的无法访问的分支。
关于c++ - gcov 报告的析构函数中的分支是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7199360/