GCC/GCOV 为 使用 throw() / noexexcept 函数生成的不同分支覆盖

Different branch coverage generated by GCC/GCOV for function using throw() / noexcept

本文关键字:分支覆盖 函数 GCOV 使用 throw GCC noexexcept      更新时间:2023-10-16

我想了解GCC编译器在使用noexceptthrow()标记非抛出函数时生成的分支。我知道noexceptthrow()之间的区别,但无法弄清楚使用throw()而不是noexcept时定义了哪些其他分支以及如何提高覆盖范围以获得 100% 的分支覆盖率?

测试示例类的代码片段:

class MyClass
{
public:
MyClass() noexcept :
iThrowFlag(false)
{}
void non_throwing_method() noexcept
{
try
{
throwing_method();
}
catch (...)
{
}
}
void throwing_method()
{
if (iThrowFlag)
{
throw std::exception();
}
}
public:
bool iThrowFlag;
};

定义了 2 个测试用例,均通过:

  1. 验证non_throwing_method是否在底层throwing_method未引发时完成,
  2. 验证non_throwing_method在基础throwing_method引发任何异常时完成。

以下是GCC/GCOV为上述代码生成的覆盖率报告,涵盖了所有行和分支(覆盖了4/4个分支):

-:    0:Source:myclass.hh
-:    0:Graph:main.gcno
-:    0:Data:main.gcda
-:    0:Runs:1
-:    0:Programs:1
-:    1:#include <iostream>
-:    2:#include <string>
-:    3:
-:    4:using namespace std;
-:    5:
-:    6:class MyClass
-:    7:{
-:    8:public:
function _ZN7MyClassC2Ev called 2 returned 100% blocks executed 100%
2:    9:    MyClass() noexcept :
2:   10:        iThrowFlag(false)
2:   11:    {}
-:   12:
function _ZN7MyClass19non_throwing_methodEv called 2 returned 100% blocks executed 100%
2:   13:    void non_throwing_method() noexcept
-:   14:    {
-:   15:        try
-:   16:        {
2:   17:            throwing_method();
call    0 returned 100%
branch  1 taken 50% (fallthrough)
branch  2 taken 50% (throw)
-:   18:        }
1:   19:        catch (...)
call    0 returned 100%
-:   20:        {
-:   21:        }
2:   22:    }
-:   23:
function _ZN7MyClass15throwing_methodEv called 2 returned 50% blocks executed 100%
2:   24:    void throwing_method()
-:   25:    {
2:   26:        if (iThrowFlag)
branch  0 taken 50% (fallthrough)
branch  1 taken 50%
-:   27:        {
1:   28:            throw std::exception();
call    0 returned 100%
call    1 returned 100%
call    2 returned 0%
-:   29:        }
1:   30:    }
-:   31:
-:   32:public:
-:   33:    bool iThrowFlag;
-:   34:};

由于目标平台不支持 C++11 和noexcept,因此与throw()说明符交换,如下所示:

class MyClass
{
public:
MyClass() throw() :
iThrowFlag(false)
{}
void non_throwing_method() throw()
{
try
{
throwing_method();
}
catch (...)
{
}
}
void throwing_method()
{
if (iThrowFlag)
{
throw std::exception();
}
}
public:
bool iThrowFlag;
};

在带有检查分支的 GCOV 输出下方 (5/8)。带有throw()的代码会生成额外的 4 个分支,这些分支很难理解并通过测试来涵盖:

-:    0:Source:myclass.hh
-:    0:Graph:main.gcno
-:    0:Data:main.gcda
-:    0:Runs:1
-:    0:Programs:1
-:    1:#include <iostream>
-:    2:#include <string>
-:    3:
-:    4:using namespace std;
-:    5:
-:    6:class MyClass
-:    7:{
-:    8:public:
function _ZN7MyClassC2Ev called 2 returned 100% blocks executed 100%
2:    9:    MyClass() throw() :
2:   10:        iThrowFlag(false)
2:   11:    {}
-:   12:
function _ZN7MyClass19non_throwing_methodEv called 2 returned 100% blocks executed 75%
2:   13:    void non_throwing_method() throw()
-:   14:    {
-:   15:        try
-:   16:        {
2:   17:            throwing_method();
call    0 returned 100%
branch  1 taken 50% (fallthrough)
branch  2 taken 50% (throw)
-:   18:        }
1:   19:        catch (...)
call    0 returned 100%
call    1 returned 100%
branch  2 taken 100% (fallthrough)
branch  3 taken 0% (throw)
branch  4 never executed
branch  5 never executed
call    6 never executed
-:   20:        {
-:   21:        }
2:   22:    }
-:   23:
function _ZN7MyClass15throwing_methodEv called 2 returned 50% blocks executed 100%
2:   24:    void throwing_method()
-:   25:    {
2:   26:        if (iThrowFlag)
branch  0 taken 50% (fallthrough)
branch  1 taken 50%
-:   27:        {
1:   28:            throw std::exception();
call    0 returned 100%
call    1 returned 100%
call    2 returned 0%
-:   29:        }
1:   30:    }
-:   31:
-:   32:public:
-:   33:    bool iThrowFlag;
-:   34:};

测试用例如下(以获取有关该用例的完整详细信息):

void test1()
{
MyClass lObj;
lObj.non_throwing_method();
}
void test2()
{
MyClass lObj;
lObj.iThrowFlag = true;
lObj.non_throwing_method();
lObj.iThrowFlag = false;
}

如果有人对所描述的行为有答案/解释,我将不胜感激。

编辑: 另外附加了程序集代码(仅函数代码:"non_throwing_method1进行比较)。

除了:

_ZN7MyClass19non_throwing_methodEv:
.LFB1253:
.loc 2 13 0
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
.cfi_lsda 0x3,.LLSDA1253
pushq   %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq    %rsp, %rbp
.cfi_def_cfa_register 6
subq    $16, %rsp
movq    %rdi, -8(%rbp)
movq    __gcov0._ZN7MyClass19non_throwing_methodEv(%rip), %rax
addq    $1, %rax
movq    %rax, __gcov0._ZN7MyClass19non_throwing_methodEv(%rip)
.loc 2 17 0
movq    -8(%rbp), %rax
movq    %rax, %rdi
.LEHB0:
call    _ZN7MyClass15throwing_methodEv
.LEHE0:
movq    __gcov0._ZN7MyClass19non_throwing_methodEv+8(%rip), %rax
addq    $1, %rax
movq    %rax, __gcov0._ZN7MyClass19non_throwing_methodEv+8(%rip)
.loc 2 22 0
jmp .L7
.L6:
movq    %rax, %rdx
movq    __gcov0._ZN7MyClass19non_throwing_methodEv+16(%rip), %rax
addq    $1, %rax
movq    %rax, __gcov0._ZN7MyClass19non_throwing_methodEv+16(%rip)
.loc 2 19 0
movq    %rdx, %rax
movq    %rax, %rdi
call    __cxa_begin_catch
movq    __gcov0._ZN7MyClass19non_throwing_methodEv+24(%rip), %rax
addq    $1, %rax
movq    %rax, __gcov0._ZN7MyClass19non_throwing_methodEv+24(%rip)
call    __cxa_end_catch
movq    __gcov0._ZN7MyClass19non_throwing_methodEv+32(%rip), %rax
addq    $1, %rax
movq    %rax, __gcov0._ZN7MyClass19non_throwing_methodEv+32(%rip)
.L7:
.loc 2 22 0
nop
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1253:
.globl  __gxx_personality_v0
.section    .gcc_except_table._ZN7MyClass19non_throwing_methodEv,"aG",@progbits,_ZN7MyClass19non_throwing_methodEv,comdat
.align 4
.LLSDA1253:
.byte   0xff
.byte   0x3
.uleb128 .LLSDATT1253-.LLSDATTD1253
.LLSDATTD1253:
.byte   0x1
.uleb128 .LLSDACSE1253-.LLSDACSB1253
.LLSDACSB1253:
.uleb128 .LEHB0-.LFB1253
.uleb128 .LEHE0-.LEHB0
.uleb128 .L6-.LFB1253
.uleb128 0x1
.LLSDACSE1253:
.byte   0x1
.byte   0
.align 4
.long   0
.LLSDATT1253:
.section    .text._ZN7MyClass19non_throwing_methodEv,"axG",@progbits,_ZN7MyClass19non_throwing_methodEv,comdat
.size   _ZN7MyClass19non_throwing_methodEv, .-_ZN7MyClass19non_throwing_methodEv
.section    .text._ZN7MyClass15throwing_methodEv,"axG",@progbits,_ZN7MyClass15throwing_methodEv,comdat
.align 2
.weak   _ZN7MyClass15throwing_methodEv
.type   _ZN7MyClass15throwing_methodEv, @function

使用 throw():

_ZN7MyClass19non_throwing_methodEv:
.LFB1253:
.loc 2 13 0
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
.cfi_lsda 0x3,.LLSDA1253
pushq   %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq    %rsp, %rbp
.cfi_def_cfa_register 6
subq    $16, %rsp
movq    %rdi, -8(%rbp)
movq    __gcov0._ZN7MyClass19non_throwing_methodEv(%rip), %rax
addq    $1, %rax
movq    %rax, __gcov0._ZN7MyClass19non_throwing_methodEv(%rip)
.loc 2 17 0
movq    -8(%rbp), %rax
movq    %rax, %rdi
.LEHB0:
call    _ZN7MyClass15throwing_methodEv
.LEHE0:
movq    __gcov0._ZN7MyClass19non_throwing_methodEv+8(%rip), %rax
addq    $1, %rax
movq    %rax, __gcov0._ZN7MyClass19non_throwing_methodEv+8(%rip)
.loc 2 22 0
jmp .L3
.L8:
movq    %rax, %rdx
movq    __gcov0._ZN7MyClass19non_throwing_methodEv+16(%rip), %rax
addq    $1, %rax
movq    %rax, __gcov0._ZN7MyClass19non_throwing_methodEv+16(%rip)
.loc 2 19 0
movq    %rdx, %rax
movq    %rax, %rdi
call    __cxa_begin_catch
movq    __gcov0._ZN7MyClass19non_throwing_methodEv+24(%rip), %rax
addq    $1, %rax
movq    %rax, __gcov0._ZN7MyClass19non_throwing_methodEv+24(%rip)
.LEHB1:
call    __cxa_end_catch
.LEHE1:
movq    __gcov0._ZN7MyClass19non_throwing_methodEv+32(%rip), %rax
addq    $1, %rax
movq    %rax, __gcov0._ZN7MyClass19non_throwing_methodEv+32(%rip)
.loc 2 22 0
jmp .L3
.L9:
cmpq    $-1, %rdx
je  .L7
movq    __gcov0._ZN7MyClass19non_throwing_methodEv+48(%rip), %rdx
addq    $1, %rdx
movq    %rdx, __gcov0._ZN7MyClass19non_throwing_methodEv+48(%rip)
movq    %rax, %rdi
.LEHB2:
call    _Unwind_Resume
.L7:
movq    __gcov0._ZN7MyClass19non_throwing_methodEv+40(%rip), %rdx
addq    $1, %rdx
movq    %rdx, __gcov0._ZN7MyClass19non_throwing_methodEv+40(%rip)
.loc 2 13 0
movq    %rax, %rdi
call    __cxa_call_unexpected
.LEHE2:
.L3:
.loc 2 22 0
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1253:
.globl  __gxx_personality_v0
.section    .gcc_except_table._ZN7MyClass19non_throwing_methodEv,"aG",@progbits,_ZN7MyClass19non_throwing_methodEv,comdat
.align 4
.LLSDA1253:
.byte   0xff
.byte   0x3
.uleb128 .LLSDATT1253-.LLSDATTD1253
.LLSDATTD1253:
.byte   0x1
.uleb128 .LLSDACSE1253-.LLSDACSB1253
.LLSDACSB1253:
.uleb128 .LEHB0-.LFB1253
.uleb128 .LEHE0-.LEHB0
.uleb128 .L8-.LFB1253
.uleb128 0x1
.uleb128 .LEHB1-.LFB1253
.uleb128 .LEHE1-.LEHB1
.uleb128 .L9-.LFB1253
.uleb128 0x3
.uleb128 .LEHB2-.LFB1253
.uleb128 .LEHE2-.LEHB2
.uleb128 0
.uleb128 0
.LLSDACSE1253:
.byte   0x1
.byte   0
.byte   0x7f
.byte   0
.align 4
.long   0
.LLSDATT1253:
.byte   0
.section    .text._ZN7MyClass19non_throwing_methodEv,"axG",@progbits,_ZN7MyClass19non_throwing_methodEv,comdat
.size   _ZN7MyClass19non_throwing_methodEv, .-_ZN7MyClass19non_throwing_methodEv
.section    .text._ZN7MyClass15throwing_methodEv,"axG",@progbits,_ZN7MyClass15throwing_methodEv,comdat
.align 2
.weak   _ZN7MyClass15throwing_methodEv
.type   _ZN7MyClass15throwing_methodEv, @function

似乎过一会儿我要回答我自己的问题了。

throw()noexcept之间的差异(从C++11到C++17)非常大。

在运行时,如果异常使函数标记为throw()调用堆栈被解开到函数的调用方,那么std::unexpected被调用以执行意外的处理程序,最后(默认行为)程序被std::terminate终止。

使用 C++11 noexcept 运行时行为略有不同:调用堆栈仅在程序执行终止之前才可能解开。没有std::unexpected叫。通常编译器只是调用std::terminate而不展开。

就是这样,这就是为什么throw()函数和noexcept函数生成不同的汇编代码,代码覆盖率工具测量不同的结果。

幸运的是,从 C++17 开始,不会再有任何惊喜了,因为动态异常规范已被弃用并从标准中删除,而throw()规范仍然有效,但它与noexcept(true)相同。