gcov报告的析构函数中的分支是什么?
What is the branch in the destructor reported by gcov?
当我使用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'
让我困扰的是"至少拍过一次:50.00%的2"。
生成的.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 taken 0% (fallthrough)"
是什么导致了这个分支,我需要在代码中做什么才能在这里获得100% ?
- g++ (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2
- gcov (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2
在典型的实现中,析构函数通常有两个分支:一个用于非动态对象的析构,另一个用于动态对象的析构。特定分支的选择是通过调用者传递给析构函数的隐藏布尔参数来执行的。它通常以0或1的形式通过寄存器。
我猜,因为在你的情况下,销毁是针对非动态对象的,所以没有采取动态分支。尝试添加Foo
类的new
-ed和delete
-ed对象,第二个分支也会被占用。
这个分支是必要的原因是植根于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
的destructor也是这样做的。
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
,而在第二个版本中,它被硬编码为始终1
。
类B
也为它生成了两个版本的析构函数。因此,编译后的程序中有4个不同的析构函数:每个类有两个析构函数。
我可以猜测,一开始编译器内部考虑的是单个"参数化"析构函数(它的工作方式与我在上面的break描述的完全一样)。然后决定将参数化析构函数拆分为两个独立的非参数化版本:一个用于0
的硬编码参数值(非动态析构函数),另一个用于1
的硬编码参数值(动态析构函数)。在非优化模式下,它通过在函数体中分配实际的参数值并保持所有分支完全完整来执行此操作。我想这在非优化代码中是可以接受的。这就是你要处理的问题。
换句话说,你的问题的答案是:在这种情况下,不可能使编译器接受所有分支。没有办法达到100%的覆盖率。有些树枝已经"枯死"了。只是在这个版本的GCC中,生成非优化代码的方法相当"懒惰"answers"松散"。
我认为可能有一种方法可以防止非优化模式下的分裂。我只是还没找到。或者,很有可能,这是不可能做到的。旧版本的GCC使用真正的参数化析构函数。也许在这个版本的GCC中,他们决定切换到双析构方法,在这样做的同时,他们以这样一种快速而肮脏的方式"重用"了现有的代码生成器,期望优化器清除无用的分支。
当你在编译时启用了优化,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…"来生成这里描述的覆盖率
- 为不同配置设置MSVC_RUNTIME_LIBRARY的正确方法是什么
- C++避免重复声明的语法是什么
- 在C++中,将大的无符号浮点数四舍五入为整数的最佳方法是什么
- 实现无开销push_back的最佳方法是什么
- C++从另一个类访问公共静态向量的正确方法是什么
- "throw expression code" 1e7 >返回 d 是什么?投掷标准::overflow_error( "too big" ) : d;意味 着?
- C++中名称篡改的目的是什么
- 在 c++ 中拥有一组结构的正确方法是什么?
- 这个指针和内存代码打印是什么?我不知道是打印垃圾还是如何打印我需要的值
- 是什么阻止DOMTimerCoordinator::NextID进入无休止的循环
- 派生类销毁的最佳实践是什么
- 这个语法std::class<>{}(arg1, arg2) 在C++中是什么意思?
- 通过JNI传递数据数组的最快方法是什么
- "using namespace std;"在C++的作用是什么?
- 在两台机器之间进行时间戳的最佳c++chrono函数是什么
- 文件系统:复制功能的速度秘诀是什么
- 用常见虚拟函数实现的任意组合来实现派生类的正确方法是什么
- 使用QQuickFramebufferObject时同步数据的最佳方式是什么
- gcov报告的析构函数中的分支是什么?
- if语句-C++中的无分支if是什么样子的