почему объектный код, сгенерированный для noexcept и throw(), одинаков в С++ 11?

Код с использованием noexcept.

//hello.cpp
class A{
public:
    A(){}
    ~A(){}
    
};
void fun() noexcept{ //c++11 style
    A a[10];
}
int main()
{
    fun();
}

Код с использованием throw() .

//hello1.cpp
class A{
public:
    A(){}
    ~A(){}
    
};
void fun() throw(){//c++98 style
    A a[10];
}
int main()
{
    fun();
}

Согласно различным онлайн-ссылкам и книге Скотта Мейера, если во время выполнения исключение покидает забаву, спецификация исключений забавы нарушается. В спецификации исключений C++98 стек вызовов раскручивается до вызывающего объекта f, и после некоторых действий, не имеющих отношения к данному случаю, выполнение программы прекращается. Со спецификацией исключений C++11 поведение во время выполнения немного отличается: стек, возможно, раскручивается только до завершения выполнения программы. Он сказал, что код, использующий noexcept, более оптимизирован, чем код, использующий throw().

Но когда я сгенерировал машинный код для вышеуказанной программы, я обнаружил, что код, сгенерированный для обоих случаев, абсолютно одинаков.

$ g++ --std=c++11 hello1.cpp -O0 -S -o throw1.s

$ g++ --std=c++11 hello.cpp -O0 -S -o throw.s

диф ниже.

$ diff throw.s throw1.s 
1c1
<   .file   "hello.cpp"
---
>   .file   "hello1.cpp"

Сгенерированный машинный код показан ниже для функции fun для обоих случаев.

.LFB1202:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        pushq   %r12
        pushq   %rbx
        subq    $16, %rsp
        .cfi_offset 12, -24
        .cfi_offset 3, -32
        leaq    -32(%rbp), %rax
        movl    $9, %ebx
        movq    %rax, %r12
        jmp     .L5
.L6:
        movq    %r12, %rdi
        call    _ZN1AC1Ev
        addq    $1, %r12
        subq    $1, %rbx
.L5:
        cmpq    $-1, %rbx
        jne     .L6
        leaq    -32(%rbp), %rbx
        addq    $10, %rbx
.L8:
        leaq    -32(%rbp), %rax
        cmpq    %rax, %rbx
        je      .L4
        subq    $1, %rbx
        movq    %rbx, %rdi
        call    _ZN1AD1Ev
        jmp     .L8
.L4:
        addq    $16, %rsp
        popq    %rbx
        popq    %r12
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE1202:
        .size   _Z3funv, .-_Z3funv
        .globl  main
        .type   main, @function

В чем преимущество использования noexcept, когда noexcept и throw генерируют один и тот же код?


person SACHIN GOYAL    schedule 19.01.2016    source источник
comment
Э... ты ничего не бросаешь.   -  person erip    schedule 19.01.2016
comment
Возможно, вам придется скомпилировать с оптимизацией, чтобы увидеть разницу.   -  person NathanOliver    schedule 19.01.2016
comment
@NathanOliver: какую оптимизацию вы бы предпочли?   -  person SACHIN GOYAL    schedule 19.01.2016
comment
@NathanOliver: я только что попробовал с O3, результат тот же. $ g++ --std=c++11 hello1.cpp -O3 -S -o throw1.s $ diff throw.s throw1.s 1c1 ‹ .file hello. cpp --- › .file hello1.cpp   -  person SACHIN GOYAL    schedule 19.01.2016
comment
@erip: Если я выброшу что-то, это будет уже другая история, как noexcept, но я видел людей, предлагающих noexcept, даже если ваш код ничего не выбрасывает.   -  person SACHIN GOYAL    schedule 19.01.2016
comment
Я получаю это с -O3. Я получаю тот же код, но это неудивительно, поскольку вы создаете массив на основе стека.   -  person NathanOliver    schedule 19.01.2016
comment
Вполне вероятно, что ни одна из функций ничего не выдает, так почему код должен быть другим?   -  person DavidW    schedule 19.01.2016
comment
Попробуйте создать исключение в fun() и посмотрите, изменит ли это что-нибудь. Компилятор, безусловно, может обнаружить, что в вашем коде не генерируется исключение, а это означает, что нет ни фактической необходимости (C++98), ни потенциальной необходимости (C++11) раскручивать стек.   -  person Peter    schedule 19.01.2016
comment
Из cppreference: noexcept не будет вызывать std::unexpected и может раскручивать стек или нет   -  person erip    schedule 19.01.2016
comment
Конечно, код будет отличаться, когда я что-то выброшу, поскольку erip pointed noexcept не будет вызывать std::unexpected и может или не может раскрутить стек. Но вы имеете в виду, что между ними нет никакой разницы до тех пор, пока я не выброшу что-нибудь? зачем мне выбрасывать из функции, которая использует noexcept (следовательно, получатель, что она ничего не выкинет?).   -  person SACHIN GOYAL    schedule 19.01.2016
comment
@SACHINGOYAL Вопрос не столько в том, почему вы хотите бросить, сколько в том, почему вы хотели бросить. Например, вы что-то пропустили. Вопрос только в том, что вы ожидаете от компилятора в таком случае? В C++11 правила изменились, чтобы позволить компилятору реализовать больше оптимизаций или, по крайней мере, не реализовать определенные гарантии.   -  person Yam Marcovic    schedule 19.01.2016
comment
@NathanOliver: какое значение имеет массив на основе стека? не могли бы вы уточнить?   -  person SACHIN GOYAL    schedule 19.01.2016
comment
Он генерирует разные коды на моем OSX 10.11 Apple LLVM версии 7.0.2 (clang-700.1.81), по крайней мере, вызов обработчика завершения и разные таблицы исключений.   -  person Jean-Baptiste Yunès    schedule 19.01.2016
comment
Когда компилятор может видеть всю вашу программу, и она проста, он может узнать, действительно ли ваш код выдает исключение. Ничего не бросает, поэтому компилятор может отказаться от обработки бросков: черт возьми, он может оптимизировать вашу программу до int main(){}. Вы должны найти способ (скажем, бросить в зависимости от пользовательского ввода - вы идете и вызываете new unguarded с количеством байтов, определенным пользователем), чтобы предотвратить такие оптимизации, в то же время допуская некоторые из них.   -  person Yakk - Adam Nevraumont    schedule 19.01.2016


Ответы (1)


Они генерируют один и тот же код, потому что вы ничего не выбрасываете. Ваша тестовая программа настолько проста, что компилятор может тривиально проанализировать ее, определить, что она не генерирует исключение и фактически вообще ничего не делает! При включенных оптимизациях (-O1 и выше) код объекта:

fun():
        rep ret
main:
        xor     eax, eax
        ret

показывает, что ваш тестовый код оптимизируется просто для самого тривиального допустимого приложения C++:

int main()
{
    return 0;
}

Если вы действительно хотите проверить разницу в генерации объектного кода для двух типов спецификаторов исключений, вам нужно использовать реальную (то есть, нетривиальную) тестовую программу. Что-то, что на самом деле генерирует исключение, и где этот выброс не может быть проанализирован с помощью небольшого анализа времени компиляции:

void fun(int args) throw()  // C++98 style
{
   if (args == 0)
   {
      throw "Not enough arguments!";
   }
   else
   {
      // do something
   }
}

int main(int argc, char** argv)
{
   fun(argc);
   return 0;
}

В этом коде исключение создается условно в зависимости от значения входного параметра (argc), переданного в функцию main. Компилятор не может знать во время компиляции, каким будет значение этого аргумента, поэтому он не может оптимизировать ни эту условную проверку, ни создание исключения. Это заставляет его генерировать код исключения и раскручивания стека.

Теперь мы можем сравнить полученный объектный код. Используя GCC 5.3 с -O3 и -std=c++11, я получаю следующее:

Стиль С++ 98 (throw())

.LC0:
        .string "Not enough arguments!"
fun(int):
        test    edi, edi
        je      .L9
        rep ret
.L9:
        push    rax
        mov     edi, 8
        call    __cxa_allocate_exception
        xor     edx, edx
        mov     QWORD PTR [rax], OFFSET FLAT:.LC0
        mov     esi, OFFSET FLAT:typeinfo for char const*
        mov     rdi, rax
        call    __cxa_throw
        add     rdx, 1
        mov     rdi, rax
        je      .L4
        call    _Unwind_Resume
.L4:
        call    __cxa_call_unexpected
main:
        sub     rsp, 8
        call    fun(int)
        xor     eax, eax
        add     rsp, 8
        ret

Стиль С++ 11 (noexcept)

.LC0:
        .string "Not enough arguments!"
fun(int) [clone .part.0]:
        push    rax
        mov     edi, 8
        call    __cxa_allocate_exception
        xor     edx, edx
        mov     QWORD PTR [rax], OFFSET FLAT:.LC0
        mov     esi, OFFSET FLAT:typeinfo for char const*
        mov     rdi, rax
        call    __cxa_throw
fun(int):
        test    edi, edi
        je      .L8
        rep ret
.L8:
        push    rax
        call    fun(int) [clone .part.0]
main:
        test    edi, edi
        je      .L12
        xor     eax, eax
        ret
.L12:
        push    rax
        call    fun(int) [clone .part.0]

Обратите внимание, что они явно отличаются. Как утверждал Мейерс и др., спецификация throw() в стиле C++98, указывающая, что функция не генерирует исключение, заставляет компилятор, соответствующий стандартам, выдавать код для очистки стека и вызова std::unexpected когда внутри этой функции генерируется исключение. Это именно то, что происходит здесь. Поскольку fun помечен как throw(), но на самом деле выполняет исключение, объектный код показывает, что компилятор вызывает вызов __cxa_call_unexpected.

Clang также соответствует стандартам и делает то же самое. Я не буду воспроизводить объектный код, потому что он длиннее и сложнее (вы можете увидеть его на отличном сайте Мэтта Годболта), но помещение в функцию спецификации исключения в стиле C++98 приводит к тому, что компилятор явно вызывает std::terminate, если функция вызывает исключение, нарушающее ее спецификацию, тогда как спецификация исключения в стиле C++11 не приводит к вызову std::terminate.

person Cody Gray    schedule 20.01.2016