1

I would like to understand the branches generated by GCC compiler when using noexcept or throw() for marking non-throwing function. I know the differences between noexcept and throw(), but cannot figure out what are the additional branches defined when using throw() instead of noexcept and how to improve the coverage to have 100% of branch coverage?

Code snippet of tested example class:

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;
};

There are 2 test cases defined, both passing:

  1. Verification if non_throwing_method completes when underlying throwing_method is not throwing,
  2. Verification if non_throwing_method completes when underlying throwing_method is throwing any exception.

Below is the coverage report generated by GCC/GCOV for above code, all the lines and branches are covered (4/4 branches covered):

        -:    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:};

As target platform does not support C++11 and noexcept, it was exchanged with throw() specifier as below:

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;
};

Below the GCOV output with examined branches (5/8). Code with throw() generates additional 4 branches, which are quite hard to understand and to cover by testing:

        -:    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:};

Test cases are below (to have full details about the case):

void test1()
{
    MyClass lObj;
    lObj.non_throwing_method();
}

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

I would appreciate if someone have an answer/explanation for described behavior.

EDIT: Additionally attached assembly code (only code of function: `non_throwing_method1 to compare).

With noexcept:

_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

With 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
pstanisz
  • 181
  • 9
  • While understanding the different branches is not that easy you may use the filter approach: for systems that don't implement C++11 you can filter the result files from lcov before feeding them to gcov, see https://stackoverflow.com/a/43726240/5027456 for an example. – Simon Sobisch Jul 10 '17 at 09:04

1 Answers1

0

Seems that I'm gonna answer my own question after a while.

The difference between throw() and noexcept (since C++11 until C++17) is quite big.

At runtime, if an exception leaves function marked as throw() the call stack is unwound to function's caller, then std::unexpected is called to execute unexpected handler and finally (default behaviour) the program is terminated by std::terminate.

With the C++11 noexcept runtime behavior is slightly different: the call stack is only possibly unwound before program execution is terminated. No std::unexpected called. Usually compilers just call std::terminate without unwinding.

And that's it, this is why throw() and noexcept functions generate different assembly code and the code coverage tools measure different results.

Fortunately, since C++17 there will be no surprises any more, as dynamic exception specification is deprecated and removed from standard, while throw() specification remains valid, but it is same as noexcept(true).

pstanisz
  • 181
  • 9