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

509

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

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

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

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

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

недействительная указка
источник
40
Код, который использует move_if_nothrow(или whatchamacallit), увидит улучшение производительности, если есть не только ход ctor.
Р. Мартиньо Фернандес
5
Это move_if_noexcept.
Никос

Ответы:

180

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

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

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

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

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

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

Pubby
источник
6
Я думаю, что std::terminateуловка все еще подчиняется модели с нулевой стоимостью. То есть, так уж получилось, что диапазон инструкций внутри noexceptфункций отображается для вызова std::terminateif, если throwвместо разматывания стека используется. Поэтому я сомневаюсь, что у него больше накладных расходов, чем при обычном отслеживании исключений.
Матье М.
4
@Klaim Посмотрите это: stackoverflow.com/a/10128180/964135 На самом деле это просто должно быть не бросать, но это noexceptгарантирует.
Пабби
3
«Если noexceptвызывается функция throw, то std::terminateвызывается, что, по-видимому, связано с небольшим объемом служебной информации»… Нет, это следует реализовать, не генерируя таблицы исключений для такой функции, которую диспетчер исключений должен перехватить и затем выручить.
Potatoswatter
8
Обработка исключений @Pubby C ++ обычно выполняется без дополнительных затрат, за исключением таблиц переходов, которые отображают потенциально выбрасывающие адреса сайтов вызовов в точки входа обработчика. Удаление этих таблиц так же близко, как и полное удаление обработки исключений. Разница лишь в размере исполняемого файла. Наверное, не стоит ничего упоминать.
Potatoswatter
26
«Хорошо, тогда используйте его, когда очевидно, что функция никогда не сработает». Я не согласен. noexceptявляется частью интерфейса функции ; Вы не должны добавлять его только потому, что ваша текущая реализация не генерируется. Я не уверен в правильном ответе на этот вопрос, но я совершенно уверен, что то, как ваша функция ведет себя сегодня, не имеет к этому никакого отношения ...
Nemo
133

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

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

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


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

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

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

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

  • немедленная выгода, noexceptдаже если зависимости не используют его (обратная совместимость)
  • разрешение спецификации noexceptпри вызове функций, которые теоретически могут генерировать, но не ожидаются для данных аргументов
Матье М.
источник
2
Может быть, я наивен, но я хотел бы представить, что функция, которая вызывает только noexceptфункции, не должна делать ничего особенного, потому что любые исключения, которые могут возникнуть, срабатывают terminateдо того, как они достигнут этого уровня. Это сильно отличается от необходимости разбираться и распространять bad_allocисключение.
8
Да, вы можете определить noexcept так, как вы предлагаете, но это было бы действительно непригодной особенностью. Многие функции могут выдавать, если определенные условия не выполняются, и вы не можете вызвать их, даже если знаете, что условия выполнены. Например, любая функция, которая может генерировать std :: invalid_argument.
tr3w
3
@MatthieuM. Немного опоздал на ответ, но тем не менее. Функции, помеченные как noexcept, могут вызывать другие функции, которые могут генерировать, обещание состоит в том, что эта функция не будет генерировать исключение, т.е. они просто должны сами обработать исключение!
Хонф
4
Я давно проголосовал за этот ответ, но прочитав и подумав об этом, у меня есть комментарий / вопрос. «Семантика перемещения» - это единственный пример, который я когда-либо видел, чтобы кто-нибудь привел noexceptполезную / хорошую идею. Я начинаю думать, что перемещение конструкции, перемещение задания и своп - единственные случаи, которые есть ... Знаете ли вы о каких-либо других?
Немо
5
@Nemo: В стандартной библиотеке она, возможно, единственная, однако в ней есть принцип, который можно использовать в другом месте. Операция перемещения - это операция, которая временно переводит некое состояние в «подвешенное состояние», и только в том случае, если оно есть, noexceptкто-то может уверенно использовать его для фрагмента данных, к которому впоследствии можно будет получить доступ. Я мог видеть эту идею где-то в другом месте, но стандартная библиотека довольно слабая в C ++, и я думаю, что она используется только для оптимизации копий элементов.
Матье М.
77

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

Почти каждая оптимизация в компиляторе использует то, что называется «потоковым графом» функции, чтобы рассуждать о том, что является законным. Потоковый граф состоит из так называемых «блоков» функции (областей кода, которые имеют один вход и один выход) и ребер между блоками, чтобы указать, куда может перейти поток. 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
}

Граф потока для этой функции отличается, если barпомечен noexcept(у выполнения нет способа перейти между концом barи оператором catch). При обозначении как noexcept, компилятор уверен, что значение x равно 5 во время функции baz - говорят, что блок x = 5 «доминирует» над блоком baz (x) без ребра изbar() оператора catch.

Затем он может сделать то, что называется «постоянное распространение», чтобы генерировать более эффективный код. Здесь, если baz встроен, операторы, использующие x, могут также содержать константы, а затем то, что раньше было оценкой во время выполнения, может быть превращено в оценку во время компиляции и т. Д.

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

Терри Махаффи
источник
3
Это на самом деле имеет значение на практике? Пример придуман, потому что ничего раньше не x = 5может бросить. Если эта часть tryблока послужит какой-либо цели, рассуждения не будут иметь места.
Potatoswatter
7
Я бы сказал, что это действительно помогает оптимизировать функции, которые содержат блоки try / catch. Пример, который я привел, хотя и надуманный, не является исчерпывающим. Суть в том, что noexcept (например, оператор throw () перед ним) помогает компиляции генерировать меньший потоковый граф (меньше ребер, меньше блоков), что является фундаментальной частью многих оптимизаций, которые он выполняет.
Терри Mahaffey
Как компилятор может распознать, что код может вызвать исключение? Является ли доступ к массиву возможным исключением?
Томас Кубес
3
@ qub1n Если компилятор может видеть тело функции, он может искать явные throwоператоры или другие подобные вещи new. Если компилятор не может видеть тело, он должен полагаться на наличие или отсутствие noexcept. Простой доступ к массиву обычно не генерирует исключения (в C ++ нет проверки границ), поэтому нет, доступ к массиву сам по себе не заставит компилятор думать, что функция генерирует исключения. (Доступ вне зоны доступа - UB, а не гарантированное исключение.)
cdhowie
@cdhowie « он должен полагаться на присутствие или отсутствие noexcept » или присутствие throw()в pre-noexcept C ++
curiousguy
57

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

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

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

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

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

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

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

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

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

Николь Болас
источник
13
Строго говоря, move_if_noexceptне будет возвращать копию, он будет возвращать const lvalue-reference, а не rvalue-reference. В общем, это заставит вызывающего сделать копию вместо перемещения, но move_if_noexceptне делает копию. Иначе отличное объяснение.
Джонатан Уэйкли
12
+1 Джонатан. Например, изменение размера вектора приведет к перемещению объектов вместо их копирования, если используется конструктор перемещения noexcept. Так что «никогда» не соответствует действительности.
Мфонтанини
4
Я имею в виду, компилятор будет генерировать лучший код в этой ситуации. OP запрашивает пример, для которого компилятор может генерировать более оптимизированное приложение. Кажется, это так (хотя это не оптимизация компилятора ).
Мфонтанини
7
@mfontanini: компилятор генерирует только лучший код, потому что компилятор вынужден компилировать другой путь кода . Это работает только потому, что std::vectorнаписано, чтобы заставить компилятор компилировать другой код. Дело не в том, что компилятор что-то обнаружил; это о том, что пользовательский код обнаруживает что-то.
Никол Болас,
3
Дело в том, что я не могу найти "оптимизацию компилятора" в цитате в начале вашего ответа. Как сказал @ChristianRau, компилятор генерирует более эффективный код, не имеет значения, что является причиной этой оптимизации. В конце концов, компилятор будет генерировать более эффективный код, не так ли? PS: я никогда не говорил, что это оптимизация компилятора, я даже говорил: «Это не оптимизация компилятора».
Мфонтанини
22

По словам Бьярне ( язык программирования C ++, 4-е издание , стр. 366):

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

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

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

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

Саурав Саху
источник
2
У вас есть источник для этой цитаты?
Антон Голов
5
@AntonGolov "Язык программирования C ++, 4-е издание", стр. 366
Расти Шеклфорд
1
Для меня это звучит так, как будто я должен добавлять noexceptкаждый раз, за ​​исключением того, что явно хочу позаботиться об исключениях. Давайте будем реальными, большинство исключений настолько невероятны и / или настолько фатальны, что спасение вряд ли разумно или возможно. Например, в приведенном примере, если распределение завершится неудачно, приложение вряд ли сможет продолжать работать правильно.
Неонит
21
  1. Есть много примеров функций, которые, я знаю, никогда не сгенерируют, но для которых компилятор не может определить это самостоятельно. Должен ли я добавить noexcept к объявлению функции во всех таких случаях?

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

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

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

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

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

  1. операции перемещения (оператор присваивания перемещения и конструкторы перемещения)
  2. операции обмена
  3. освобождение памяти (оператор delete, оператор delete [])
  4. деструкторы (хотя это неявно, noexcept(true)если вы их не делаете noexcept(false))

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

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

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

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

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

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

Я также рекомендую книгу Скотта Мейерса «Effective Modern C ++», «Пункт 14: Объявляйте функции без исключения, если они не будут генерировать исключения» для дальнейшего чтения.

Филипп Классен
источник
Тем не менее, было бы гораздо больше смысла, если бы исключения были реализованы в C ++, как в Java, где вы помечаете метод, который может выдавать throwsключевое слово вместо noexceptотрицательного. Я просто не могу получить некоторые из вариантов дизайна C ++ ...
док
Они назвали это, noexceptпотому что throwбыл уже взят. Проще говоря, throwих можно использовать почти так, как вы упомянули, за исключением того, что они испортили его дизайн, так что он стал почти бесполезным - даже вредным. Но мы застряли с этим сейчас, поскольку его устранение было бы серьезным изменением с небольшой выгодой. Так noexceptв принципе throw_v2.
AnorZaken
Как это throwне полезно?
любопытный парень
@curiousguy «throw» само по себе (для выброса исключений) полезно, но «throw» как спецификатор исключений устарело, а в C ++ 17 даже удалено. По причинам, по которым спецификаторы исключений бесполезны, см. Этот вопрос: stackoverflow.com/questions/88573/…
Philipp Claßen
1
@ PhilippClaßen Спецификатор throw()исключения не предоставляет такую ​​же гарантию, как nothrow?
любопытный парень
17

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

Когда вы говорите «Я знаю, что [они] никогда не сгенерируют», вы имеете в виду, изучая реализацию функции, вы знаете, что функция не сгенерирует. Я думаю, что этот подход наизнанку.

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

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