Когда я должен действительно использовать noexcept?

362

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

  • Есть много примеров функций, которые, как я знаю, никогда не будут бросать, но для которых компилятор не может определить это самостоятельно. Должен ли я добавить noexcept к объявлению функции в всех таких случаях?

    Чтобы подумать о том, нужно ли добавлять noexcept после объявления каждого, значительно снизит производительность программиста (и, откровенно говоря, будет боль в заднице). В каких ситуациях я должен быть более осторожным в отношении использования noexcept, и для каких ситуаций я могу уйти с подразумеваемой noexcept(false)?

  • Когда я могу реально ожидать улучшения производительности после использования noexcept? В частности, дайте пример кода, для которого компилятор С++ способен генерировать более эффективный машинный код после добавления noexcept.

    Лично я забочусь о noexcept из-за большей свободы, предоставляемой компилятору, для безопасного применения определенных видов оптимизации. Таким образом, современные компиляторы используют noexcept? Если нет, могу ли я ожидать, что некоторые из них сделают это в ближайшем будущем?

  • 31
    Код, который использует move_if_nothrow (или whatchamacallit), увидит улучшение производительности, если не будет никакого ctor хода.
  • 0
    Связанный: github.com/isocpp/CppCoreGuidelines/blob/master/…
Показать ещё 1 комментарий
Теги:
c++11
exception
exception-handling
noexcept

8 ответов

137
Лучший ответ

Я думаю, что слишком рано давать ответ "наилучшей практики" для этого, поскольку на практике не было достаточно времени для его использования. Если бы это спросили о спецификаторах броска сразу после того, как они вышли, тогда ответы были бы очень разными.

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

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

Когда я могу реально ожидать улучшения производительности после использования noexcept? [...] Лично я забочусь о noexcept, потому что увеличенная свобода предоставила компилятору безопасное применение определенных видов оптимизации.

Похоже, что наибольшая оптимизация достигается за счет оптимизации пользователей, а не от компиляторов из-за возможности проверки noexcept и перегрузки на нем. Большинство компиляторов следуют методу обработки исключений без штрафа-if-you-don-th-throw, поэтому я сомневаюсь, что он сильно изменится (или что-то еще) на уровне машинного кода вашего кода, хотя, возможно, уменьшит размер двоичного файла, удалив обработку код.

Использование noexcept в большом 4 (конструкторы, назначение, а не деструкторы, поскольку они уже noexcept), скорее всего, приведут к лучшим улучшениям, так как проверки noexcept являются "общими" в шаблоне кода, например, в std-контейнерах, Например, std::vector не будет использовать перемещение вашего класса, если не будет помечено noexcept (или компилятор может вывести его в противном случае).

  • 4
    Я думаю, что трюк std::terminate все еще подчиняется модели с нулевой стоимостью. То есть, так получилось, что диапазон инструкций в функциях noexcept сопоставлен с вызовом std::terminate если используется throw вместо разматывателя стека. Поэтому я сомневаюсь, что он имеет больше накладных расходов, чем обычное отслеживание исключений.
  • 0
    «Например, std :: vector не будет использовать ход вашего класса, если он не помечен как noexcept». Какие? В самом деле? Вы уверены, что это требуется?
Показать ещё 13 комментариев
110

Как я повторяю в эти дни: семантика первая.

Добавление noexcept, noexcept(true) и noexcept(false) - это прежде всего семантика. Это только случайно обусловливает ряд возможных оптимизаций.

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


Хорошо, теперь о возможных возможностях оптимизации.

Наиболее очевидные оптимизации фактически выполняются в библиотеках. С++ 11 предоставляет ряд признаков, которые позволяют узнать, является ли функция noexcept или нет, и сама реализация стандартной библиотеки будет использовать эти черты для поддержки операций noexcept на пользовательских объектах, которыми они манипулируют, если это возможно. Такие как семантика перемещения.

Компилятор может только сбрить немного жира (возможно) из данных обработки исключений, поскольку он должен учитывать тот факт, что вы, возможно, солгали. Если функция, помеченная noexcept, выполняет команду throw, вызывается std::terminate.

Эти семантики были выбраны по двум причинам:

  • сразу же получает выгоду от noexcept, даже если зависимости не используют его (обратная совместимость)
  • позволяет специфицировать noexcept при вызове функций, которые теоретически могут быть выбраны, но не ожидаются для данных аргументов
  • 2
    Может быть, я наивен, но я хотел бы представить, что функция, которая вызывает только функции noexcept , не должна была бы делать ничего особенного, потому что любые исключения, которые могут возникнуть, terminate до того, как достигают этого уровня. Это сильно отличается от необходимости обрабатывать и распространять исключение bad_alloc .
  • 0
    @Hurkyl: ты прав. Вот почему достаточно умный компилятор мог бы понять это и избежать генерации записей в таблице счетчик программ / обработчик исключений для этой функции (таким образом уменьшая жирность, как я уже сказал). Однако многие функции не будут помечены как noexcept (например, подумайте о системных вызовах или низкоуровневых C-специфичных для ОС функциях), поэтому был выбран ярлык, заключающийся в том, что компилятору не приходилось его проверять.
Показать ещё 7 комментариев
60

На самом деле это делает (потенциально) огромную разницу с оптимизатором в компиляторе. Составители на самом деле имели эту функцию в течение многих лет через инструкцию empty() после определения функции, а также приличительные расширения. Я могу заверить вас, что современные компиляторы используют эти знания для создания лучшего кода.

Почти каждая оптимизация в компиляторе использует что-то, называемое "потоковым графом" функции, чтобы рассуждать о том, что является законным. График потока состоит из так называемых "блоков" функции (областей кода, которые имеют один вход и один выход) и границ между блоками, чтобы указать, к чему может перейти поток. Noexcept изменяет график потока.

Вы попросили указать конкретный пример. Рассмотрим этот код:

void foo(int x) {
    try {
        bar();
        x = 5;
        // other stuff which doesn't modify x, but might throw
    } catch(...) {
        // don't modify x
    }

    baz(x); // or other statement using x
}

График потока для этой функции различен, если бар помечен noexcept (невозможно выполнить переход между концом строки и оператором catch). Если помечено как noexcept, компилятор уверен, что значение x равно 5 во время функции baz - блок x = 5 считается "доминирующим" блоком baz (x) без края из bar() в оператор catch. Затем он может сделать что-то, называемое "постоянным распространением", чтобы генерировать более эффективный код. Здесь, если baz inlined, операторы, использующие x, также могут содержать константы, а затем то, что раньше было оценкой времени выполнения, можно превратить в оценку времени компиляции и т.д.

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

  • 3
    Это на самом деле имеет значение на практике? Пример придуман, потому что ничто до x = 5 может бросить. Если эта часть блока try послужит какой-либо цели, рассуждения не сохранятся.
  • 6
    Я бы сказал, что это действительно помогает оптимизировать функции, которые содержат блоки try / catch. Пример, который я привел, хотя и надуманный, не является исчерпывающим. Суть в том, что noexcept (например, оператор throw () перед ним) помогает компиляции генерировать меньший потоковый граф (меньше ребер, меньше блоков), что является фундаментальной частью многих оптимизаций, которые он выполняет.
Показать ещё 4 комментария
42

noexcept может значительно повысить производительность некоторых операций. Это не происходит на уровне генерирования машинного кода компилятором, но, выбирая наиболее эффективный алгоритм: как упоминалось выше, вы делаете этот выбор с помощью функции std::move_if_noexcept. Например, рост std::vector (например, при вызове reserve) должен обеспечить сильную гарантию безопасности исключений. Если он знает, что конструктор перемещения T не бросает, он может просто перемещать каждый элемент. В противном случае он должен скопировать все T s. Это подробно описано в этом сообщении.

  • 3
    Приложение: Это означает, что если вы определяете конструкторы перемещения или операторы присваивания перемещения, добавляйте к ним noexcept (если применимо)! Неявно определенные функции-члены перемещения не имеют noexcept добавленного к ним автоматически (если применимо).
26

Когда я могу реалистично, кроме как наблюдать улучшение производительности после использования noexcept? В частности, дайте пример кода, для которого компилятор С++ может генерировать более лучший машинный код после добавления noexcept.

Ум, никогда? Никогда не время? Никогда.

noexcept предназначен для оптимизации производительности компилятора таким же образом, что const предназначен для оптимизации производительности компилятора. То есть, почти никогда.

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

Шаблоны, такие как move_if_noexcept, обнаруживают, если конструктор перемещения определен с помощью noexcept и вернет const& вместо && этого типа, если это не так. Это способ сказать, чтобы двигаться, если это очень безопасно.

В общем, вы должны использовать noexcept, когда считаете, что это действительно будет полезно для этого. Некоторый код будет принимать разные пути, если is_nothrow_constructible true для этого типа. Если вы используете код, который сделает это, тогда не стесняйтесь noexcept соответствующие конструкторы.

Короче: используйте его для конструкторов перемещения и подобных конструкций, но не чувствуйте, что вам нужно сходить с ума.

  • 12
    Строго move_if_noexcept , move_if_noexcept не будет возвращать копию, она будет возвращать const lvalue-reference, а не rvalue-reference. В общем, это заставит вызывающего сделать копию вместо перемещения, но move_if_noexcept не делает копию. В противном случае, отличное объяснение.
  • 10
    +1 Джонатан. Например, изменение размера вектора приведет к перемещению объектов вместо их копирования, если конструктор перемещения не является noexcept . Так что «никогда» не соответствует действительности.
Показать ещё 8 комментариев
17
  • Есть много примеров функций, которые, как я знаю, никогда не будут бросать, но для которых компилятор не может определить это самостоятельно. Должен ли я добавить noexcept в объявление функции во всех таких случаях?

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

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

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

  1. В каких ситуациях я должен быть более осторожным в отношении использования noexcept и для каких ситуаций я могу уйти с подразумеваемой noexcept (false)?

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

  • операции перемещения (перемещение оператора присваивания и перемещение конструкторов)
  • операции свопинга
  • деаллокаторы памяти (оператор delete, оператор delete [])
  • деструкторы (хотя они неявно noexcept(true), если вы не сделаете их noexcept(false))

Эти функции обычно должны быть noexcept, и, скорее всего, реализации библиотеки могут использовать свойство noexcept. Например, std::vector может использовать операции без металирования, не жертвуя сильными исключениями. В противном случае он должен будет вернуться к копированию элементов (как это было на С++ 98).

Этот вид оптимизации находится на уровне алгоритма и не зависит от оптимизации компилятора. Это может оказать существенное влияние, особенно если элементы дороги для копирования.

  1. Когда я могу реально ожидать улучшения производительности после использования noexcept? В частности, дайте пример кода, для которого компилятор С++ способен генерировать более лучший машинный код после добавления noexcept.

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

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

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

Я также рекомендую книгу Скотта Мейерса "Эффективный современный С++", "Пункт 14: Объявить функции noexcept, если они не будут испускать исключения" для дальнейшего чтения.

  • 0
    Тем не менее, было бы гораздо больше смысла, если бы исключения были реализованы в C ++, как в Java, где вы помечаете метод, который может throws ключевое слово throws вместо noexcept . Я просто не могу получить некоторые из вариантов дизайна C ++ ...
  • 0
    Они назвали это noexcept потому что throw уже был взят. Проще говоря, throw можно использовать почти так, как вы упомянули, за исключением того, что он испортил его дизайн, так что он стал почти бесполезным - даже вредным. Но мы застряли с этим сейчас, поскольку его устранение было бы серьезным изменением с небольшой выгодой. Так что, noexcept как в основном, throw_v2 .
Показать ещё 5 комментариев
10

В словах Бьярне:

Если окончание является приемлемым ответом, неперехваченное исключение достигнет этого, потому что он превращается в вызов terminate() (§13.5.2.5). Кроме того, спецификатор noexcept (§13.5.1.1) может сделать это желание явно.

Успешные отказоустойчивые системы являются многоуровневыми. Каждый уровень справляется с таким количеством ошибок, сколько может, без слишком искаженного и уходящего остальные - на более высокие уровни. Исключения поддерживают эту точку зрения. Более того, terminate() поддерживает этот вид, предоставляя escape, если сам механизм обработки исключений поврежден или если он был не полностью используемые, что исключает исключение исключений. По аналогии, noexcept обеспечивает простой escape-код для ошибок при попытке восстановления кажется неосуществимым.

 double compute(double x) noexcept;   {       
     string s = "Courtney and Anya"; 
     vector<double> tmp(10);      
     // ...   
 }

Конструктор векторов может не получить память для своих десяти парных и выбросьте std::bad_alloc. В этом случае программа завершается. Это безоговорочно завершается путем вызова std::terminate() (§30.4.1.3). Он не вызывает деструкторы от вызывающих функций. это реализация определяет, будут ли деструкторы из областей между throw и noexcept (например, для s в compute()). программа вот-вот закончится, поэтому мы не должны зависеть от каких-либо объект в любом случае. Добавив спецификатор noexcept, мы укажем, что наш код не был написан, чтобы справиться с броском.

  • 1
    У вас есть источник для этой цитаты?
  • 4
    @AntonGolov "Язык программирования C ++, 4-е издание", стр. 366
10

Есть много примеров функций, которые, как я знаю, никогда не будут бросать, но для которых компилятор не может определить это самостоятельно. Должен ли я добавить noexcept в объявление функции во всех таких случаях?

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

Лучше подумать о том, может ли функция генерировать исключения как часть конструкции функции: такая же важная, как и список аргументов, и является ли метод мутатором (... const). Объявление о том, что "эта функция никогда не вызывает исключений", является ограничением реализации. Опущение это не означает, что функция может генерировать исключения; это означает, что текущая версия функции и все будущие версии могут вызывать исключения. Это ограничение, которое усложняет реализацию. Но некоторые методы должны иметь ограничение, чтобы быть практически полезным; самое главное, поэтому их можно вызвать из деструкторов, но также и для реализации кода "отката" в методах, которые обеспечивают надежную гарантию исключения.

  • 0
    Это лучший ответ на сегодняшний день. Вы даете гарантию пользователям вашего метода, что является еще одним способом сказать, что вы ограничиваете свою реализацию навсегда (без серьезных изменений). Спасибо за интересную перспективу.
  • 0
    Смотрите также связанный вопрос Java .

Ещё вопросы

Сообщество Overcoder
Наверх
Меню