Имеет ли смысл объявлять встроенные функции noexcept?

Из того, что я могу сказать, сообщество SO делится на то, объявляет ли функция noexcept значимую оптимизацию компилятора, которая в противном случае была бы невозможна. (Я говорю конкретно об оптимизации компилятора, а не об оптимизации реализации библиотеки на основе move_if_noexcept.) Для целей этого вопроса предположим, что noexcept делает возможной оптимизацию генерации кода. С этим допущением имеет смысл объявить inline функции noexcept? Предполагая, что такие функции фактически встроены, это, по-видимому, требует, чтобы компиляторы генерировали эквивалент блока try вокруг кода, полученного в результате функции inline на сайте вызова, потому что, если в этой области возникает исключение, terminate должен быть вызван. Без noexcept этот блок try кажется ненужным.

Мой первоначальный интерес заключался в том, было ли смысл объявлять функции лямбда noexcept, учитывая, что они неявно inline, но потом я понял, что одни и те же проблемы возникают для любой функции inline, а не только для Lambdas.

Ответы

Ответ 1

допустим, что noexcept действительно делает возможной оптимизацию генерации кода

OK

Предполагая, что такие функции действительно включены, это, казалось бы, требуют, чтобы компиляторы генерировали эквивалент блока try вокруг код, полученный в результате встроенной функции на сайте вызова, потому что если в этой области возникает исключение

Не обязательно, потому что может быть, что компилятор может смотреть на тело функции и видеть, что он ничего не может выбросить. Поэтому номинальная обработка исключений может быть отменена.

Если функция "полностью" встроена (то есть, если встроенный код не содержит вызовов функций), я бы ожидал, что компилятор может довольно часто делать это определение, но не, например, в случае, когда есть вызов до vector::push_back(), и писатель функции знает, что зарезервировано достаточное пространство, но компилятор этого не делает.

Помните также, что в хорошей реализации блок try может фактически не потребовать выполнения какого-либо кода в случае, когда ничего не выбрано.

С этим предположением имеет смысл объявлять встроенные функции noexcept?

Да, чтобы получить все предполагаемые оптимизации noexcept.

Ответ 2

Стоит отметить, что в кругах власти была интересная дискуссия о проблемах, не связанных с норой. Я настоятельно рекомендую прочитать их:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2010/n3227.html

http://www.stroustrup.com/N3202-noexcept.pdf

По-видимому, весьма влиятельные люди заинтересованы в добавлении своего рода автоматического вывода nothrow в С++.

После некоторого размышления я изменил свое положение почти на противоположное, ниже

Рассмотрим это:

  • когда вы вызываете функцию, которая имеет noexcept в объявлении, вам это выгодно (не нужно иметь дело с возможностью разворота и т.д.)

  • когда компилятор компилирует функцию, которая имеет noexcept on definion - (если компилятор не может доказать, что функция действительно незначительная) производительность страдает (теперь компилятор должен гарантировать, что исключение не сможет избежать этой функции). Вы просите его обеспечить соблюдение обещаний без исключений.

т.е. noexcept оба причиняют вам боль и приносят пользу. Это не так, если функция встроена! Когда он встроен - нет никакой пользы от noexcept от объявления вообще (декларация и определение становятся одной вещью). То есть, если вы на самом деле не хотите, чтобы компилятор обеспечивал это ради безопасности. И по безопасности я имею в виду, что вы скорее закончите, чем произведете неправильный результат.

Теперь должно быть очевидно - нет смысла объявлять встроенные функции noexcept (имейте в виду, что не каждая встроенная функция будет входить в нее).

Давайте посмотрим на разные категории функций, которые не выбрасывают (вы просто знаете, что они этого не делают):

  • non-inlined, компилятор может доказать, что он не бросает - noexcept не повредит тело функции (компилятор просто проигнорирует спецификацию), и сайты-вызовы выиграют от этого обещания

  • non-inlined, компилятор не может доказать, что он не выбрасывает - noexcept может повредить тело функции, но выгодные сайты вызова (трудно сказать, что более выгодно)

  • inlined, компилятор может доказать, что он не бросает - noexcept не служит цели

  • inlined, компилятор не может доказать, что он не бросает - noexcept повредит сайт вызова

Как вы видите, nothrow - это просто плохо разработанная языковая функция. Он работает только в том случае, если вы хотите выполнить обещание без исключений. Нельзя использовать его правильно - он может дать вам "безопасность", но не производительность.

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

Итак, что делать?

  • объявить о своем поведении (увы, язык не может вам помочь)! Например:.

    void push_back(int k);   // throws only if there is no unused memory
    
  • не ставьте noexcept на встроенные функции (если это маловероятно, чтобы быть встроенным, например очень большим)

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

  • используйте noexcept для перемещения конструктора и переместите оператор присваивания (и деструктор?). Это может повлиять на них отрицательно, но если вы этого не сделаете - некоторые функции библиотеки (std:: swap, некоторые операции с контейнерами) не будут использовать наиболее эффективный путь (или не будут предоставлять лучшую гарантию исключения). В принципе, любое место, использующее noexcept оператора для вашей функции (на данный момент), заставит вас использовать noexcept спецификатор.

  • используйте noexcept, если вы не доверяете вызовам, которые выполняет ваша функция, и скорее умрут, чем они себя ведут неожиданно

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

Ну, как иначе noexcept можно было бы спроектировать?

  • Я бы использовал два разных ключевых слова - один для объявления обещания, а другой для его принудительного применения. Например. noexcept и force_noexcept. Фактически, принудительное выполнение на самом деле не требуется - это можно сделать с помощью try/catch + terminate() (да, он разворачивает стек, но кто волнуется, если за ним следует std:: terminate()?)

  • Я бы заставил компилятор проанализировать все вызовы в заданной функции, чтобы определить, может ли он бросить. Если это так, и было сделано безусловное обещание - ошибка компилятора будет выпущена

  • Для кода, который может быть брошен, но вы знаете, что этого не должно быть, чтобы обеспечить компилятор, чтобы он был в порядке. Smth вот так:

    vector<int> v;
    v.reserve(1);
    ...
    nothrow {       // memory pre-allocated, this won't throw
        v.push_back(10);
    }
    

    если обещание нарушено (т.е. кто-то изменил векторный код и теперь он предоставляет другие гарантии) - поведение undefined.

Отказ от ответственности: этот подход может быть слишком непрактичным, кто знает...