Ключевое слово noexcept
может быть соответствующим образом применено ко многим подписям функций, но я не уверен, когда я должен использовать его на практике. Основываясь на том, что я прочитал до сих пор, добавление noexcept
в последнюю минуту, похоже, затрагивает некоторые важные проблемы, возникающие при броске конструкторов. Тем не менее, я все еще не могу дать удовлетворительные ответы на некоторые практические вопросы, которые привели меня к тому, чтобы я больше читал о noexcept
в первую очередь.
Есть много примеров функций, которые, как я знаю, никогда не будут бросать, но для которых компилятор не может определить это самостоятельно. Должен ли я добавить noexcept
к объявлению функции в всех таких случаях?
Чтобы подумать о том, нужно ли добавлять noexcept
после объявления каждого, значительно снизит производительность программиста (и, откровенно говоря, будет боль в заднице). В каких ситуациях я должен быть более осторожным в отношении использования noexcept
, и для каких ситуаций я могу уйти с подразумеваемой noexcept(false)
?
Когда я могу реально ожидать улучшения производительности после использования noexcept
? В частности, дайте пример кода, для которого компилятор С++ способен генерировать более эффективный машинный код после добавления noexcept
.
Лично я забочусь о noexcept
из-за большей свободы, предоставляемой компилятору, для безопасного применения определенных видов оптимизации. Таким образом, современные компиляторы используют noexcept
? Если нет, могу ли я ожидать, что некоторые из них сделают это в ближайшем будущем?
Я думаю, что слишком рано давать ответ "наилучшей практики" для этого, поскольку на практике не было достаточно времени для его использования. Если бы это спросили о спецификаторах броска сразу после того, как они вышли, тогда ответы были бы очень разными.
Чтобы подумать, нужно ли добавлять
noexcept
после того, как каждое объявление функции значительно снизит производительность программиста (и, откровенно говоря, будет боль).
Ну, тогда используйте его, когда очевидно, что функция никогда не будет бросать.
Когда я могу реально ожидать улучшения производительности после использования
noexcept
? [...] Лично я забочусь оnoexcept
, потому что увеличенная свобода предоставила компилятору безопасное применение определенных видов оптимизации.
Похоже, что наибольшая оптимизация достигается за счет оптимизации пользователей, а не от компиляторов из-за возможности проверки noexcept
и перегрузки на нем. Большинство компиляторов следуют методу обработки исключений без штрафа-if-you-don-th-throw, поэтому я сомневаюсь, что он сильно изменится (или что-то еще) на уровне машинного кода вашего кода, хотя, возможно, уменьшит размер двоичного файла, удалив обработку код.
Использование noexcept
в большом 4 (конструкторы, назначение, а не деструкторы, поскольку они уже noexcept
), скорее всего, приведут к лучшим улучшениям, так как проверки noexcept
являются "общими" в шаблоне кода, например, в std-контейнерах, Например, std::vector
не будет использовать перемещение вашего класса, если не будет помечено noexcept
(или компилятор может вывести его в противном случае).
std::terminate
все еще подчиняется модели с нулевой стоимостью. То есть, так получилось, что диапазон инструкций в функциях noexcept
сопоставлен с вызовом std::terminate
если используется throw
вместо разматывателя стека. Поэтому я сомневаюсь, что он имеет больше накладных расходов, чем обычное отслеживание исключений.
Как я повторяю в эти дни: семантика первая.
Добавление noexcept
, noexcept(true)
и noexcept(false)
- это прежде всего семантика. Это только случайно обусловливает ряд возможных оптимизаций.
Как код чтения программиста, наличие noexcept
сродни значению const
: это помогает мне лучше понять, что может или не может произойти. Поэтому стоит потратить некоторое время на размышления о том, знаете ли вы, будет ли функция бросать. Для напоминания может выделяться любое распределение динамической памяти.
Хорошо, теперь о возможных возможностях оптимизации.
Наиболее очевидные оптимизации фактически выполняются в библиотеках. С++ 11 предоставляет ряд признаков, которые позволяют узнать, является ли функция noexcept
или нет, и сама реализация стандартной библиотеки будет использовать эти черты для поддержки операций noexcept
на пользовательских объектах, которыми они манипулируют, если это возможно. Такие как семантика перемещения.
Компилятор может только сбрить немного жира (возможно) из данных обработки исключений, поскольку он должен учитывать тот факт, что вы, возможно, солгали. Если функция, помеченная noexcept
, выполняет команду throw, вызывается std::terminate
.
Эти семантики были выбраны по двум причинам:
noexcept
, даже если зависимости не используют его (обратная совместимость)noexcept
при вызове функций, которые теоретически могут быть выбраны, но не ожидаются для данных аргументовnoexcept
, не должна была бы делать ничего особенного, потому что любые исключения, которые могут возникнуть, terminate
до того, как достигают этого уровня. Это сильно отличается от необходимости обрабатывать и распространять исключение bad_alloc
.
noexcept
(например, подумайте о системных вызовах или низкоуровневых C-специфичных для ОС функциях), поэтому был выбран ярлык, заключающийся в том, что компилятору не приходилось его проверять.
На самом деле это делает (потенциально) огромную разницу с оптимизатором в компиляторе. Составители на самом деле имели эту функцию в течение многих лет через инструкцию 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).
x = 5
может бросить. Если эта часть блока try
послужит какой-либо цели, рассуждения не сохранятся.
noexcept
может значительно повысить производительность некоторых операций. Это не происходит на уровне генерирования машинного кода компилятором, но, выбирая наиболее эффективный алгоритм: как упоминалось выше, вы делаете этот выбор с помощью функции std::move_if_noexcept
. Например, рост std::vector
(например, при вызове reserve
) должен обеспечить сильную гарантию безопасности исключений. Если он знает, что конструктор перемещения T
не бросает, он может просто перемещать каждый элемент. В противном случае он должен скопировать все T
s. Это подробно описано в этом сообщении.
noexcept
(если применимо)! Неявно определенные функции-члены перемещения не имеют noexcept
добавленного к ним автоматически (если применимо).
Когда я могу реалистично, кроме как наблюдать улучшение производительности после использования
noexcept
? В частности, дайте пример кода, для которого компилятор С++ может генерировать более лучший машинный код после добавления noexcept.
Ум, никогда? Никогда не время? Никогда.
noexcept
предназначен для оптимизации производительности компилятора таким же образом, что const
предназначен для оптимизации производительности компилятора. То есть, почти никогда.
noexcept
в основном используется, чтобы "вы" обнаруживали во время компиляции, если функция может генерировать исключение. Помните: большинство компиляторов не выделяют специальный код для исключений, если он на самом деле ничего не бросает. Таким образом, noexcept
- это не вопрос подсказки компилятора о том, как оптимизировать функцию, так как дает вам подсказки о том, как использовать функцию.
Шаблоны, такие как move_if_noexcept
, обнаруживают, если конструктор перемещения определен с помощью noexcept
и вернет const&
вместо &&
этого типа, если это не так. Это способ сказать, чтобы двигаться, если это очень безопасно.
В общем, вы должны использовать noexcept
, когда считаете, что это действительно будет полезно для этого. Некоторый код будет принимать разные пути, если is_nothrow_constructible
true для этого типа. Если вы используете код, который сделает это, тогда не стесняйтесь noexcept
соответствующие конструкторы.
Короче: используйте его для конструкторов перемещения и подобных конструкций, но не чувствуйте, что вам нужно сходить с ума.
move_if_noexcept
, move_if_noexcept
не будет возвращать копию, она будет возвращать const lvalue-reference, а не rvalue-reference. В общем, это заставит вызывающего сделать копию вместо перемещения, но move_if_noexcept
не делает копию. В противном случае, отличное объяснение.
noexcept
. Так что «никогда» не соответствует действительности.
- Есть много примеров функций, которые, как я знаю, никогда не будут бросать, но для которых компилятор не может определить это самостоятельно. Должен ли я добавить noexcept в объявление функции во всех таких случаях?
noexcept
является сложным, так как он является частью интерфейса функций. Особенно, если вы пишете библиотеку, ваш клиентский код может зависеть от свойства noexcept
. Это может быть трудно изменить позже, так как вы можете сломать существующий код. Это может быть менее опасно, если вы используете код, который используется только вашим приложением.
Если у вас есть функция, которая не может выбраться, спросите себя, будет ли она оставаться noexcept
или это ограничит будущие реализации? Например, вы можете ввести проверку ошибок незаконных аргументов, выбрасывая исключения (например, для модульных тестов), или вы можете зависеть от другого кода библиотеки, который мог бы изменить его спецификацию исключения. В этом случае безопаснее быть консервативным и опускать noexcept
.
С другой стороны, если вы уверены, что функция никогда не должна бросать, и правильно, что она является частью спецификации, вы должны объявить ее noexcept
. Однако имейте в виду, что компилятор не сможет обнаружить нарушения noexcept
, если ваша реализация изменится.
- В каких ситуациях я должен быть более осторожным в отношении использования noexcept и для каких ситуаций я могу уйти с подразумеваемой noexcept (false)?
Существует четыре класса функций, над которыми вам следует сосредоточиться, потому что они, вероятно, окажут наибольшее влияние:
noexcept(true)
, если вы не сделаете их noexcept(false)
)Эти функции обычно должны быть noexcept
, и, скорее всего, реализации библиотеки могут использовать свойство noexcept
. Например, std::vector
может использовать операции без металирования, не жертвуя сильными исключениями. В противном случае он должен будет вернуться к копированию элементов (как это было на С++ 98).
Этот вид оптимизации находится на уровне алгоритма и не зависит от оптимизации компилятора. Это может оказать существенное влияние, особенно если элементы дороги для копирования.
- Когда я могу реально ожидать улучшения производительности после использования noexcept? В частности, дайте пример кода, для которого компилятор С++ способен генерировать более лучший машинный код после добавления noexcept.
Преимущество noexcept
от спецификации исключения или throw()
заключается в том, что стандарт позволяет компиляторам больше свободы, когда дело доходит до разворачивания стека. Даже в случае throw()
компилятор должен полностью разматывать стек (и он должен делать это в точном обратном порядке конструкций объекта).
В случае noexcept
, с другой стороны, от этого не требуется. Нет требования, чтобы стек был размотан (но компилятору все же разрешено это делать). Эта свобода позволяет использовать дополнительную оптимизацию кода, поскольку она снижает накладные расходы, позволяя всегда разматывать стек.
Связанный с этим вопрос о noexcept, разворачивании стека и производительности содержит более подробные сведения о накладных расходах, когда требуется разматывание стека.
Я также рекомендую книгу Скотта Мейерса "Эффективный современный С++", "Пункт 14: Объявить функции noexcept, если они не будут испускать исключения" для дальнейшего чтения.
throws
ключевое слово throws
вместо noexcept
. Я просто не могу получить некоторые из вариантов дизайна C ++ ...
noexcept
потому что throw
уже был взят. Проще говоря, throw
можно использовать почти так, как вы упомянули, за исключением того, что он испортил его дизайн, так что он стал почти бесполезным - даже вредным. Но мы застряли с этим сейчас, поскольку его устранение было бы серьезным изменением с небольшой выгодой. Так что, noexcept
как в основном, throw_v2
.
В словах Бьярне:
Если окончание является приемлемым ответом, неперехваченное исключение достигнет этого, потому что он превращается в вызов 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
, мы укажем, что наш код не был написан, чтобы справиться с броском.
Есть много примеров функций, которые, как я знаю, никогда не будут бросать, но для которых компилятор не может определить это самостоятельно. Должен ли я добавить noexcept в объявление функции во всех таких случаях?
Когда вы говорите: "Я знаю, что они никогда не будут бросать", вы имеете в виду, исследуя реализацию функции, которую знаете, что функция не будет выбрасывать. Я думаю, что этот подход наизнанку.
Лучше подумать о том, может ли функция генерировать исключения как часть конструкции функции: такая же важная, как и список аргументов, и является ли метод мутатором (... const
). Объявление о том, что "эта функция никогда не вызывает исключений", является ограничением реализации. Опущение это не означает, что функция может генерировать исключения; это означает, что текущая версия функции и все будущие версии могут вызывать исключения. Это ограничение, которое усложняет реализацию. Но некоторые методы должны иметь ограничение, чтобы быть практически полезным; самое главное, поэтому их можно вызвать из деструкторов, но также и для реализации кода "отката" в методах, которые обеспечивают надежную гарантию исключения.
move_if_nothrow
(или whatchamacallit), увидит улучшение производительности, если не будет никакого ctor хода.