Когда действительно стоит использовать noexcept?
Ключевое слово noexcept
может быть применено к многим сигнатурам функций, но я не уверен, когда именно следует использовать его на практике. Из прочитанного мною стало понятно, что добавление noexcept
в последний момент решает некоторые важные проблемы, возникающие, когда конструкторы перемещения выбрасывают исключения. Однако у меня все еще остаются нерешенные практические вопросы, которые изначально и привели меня к изучению noexcept
.
-
Существует множество примеров функций, которые, как я знаю, никогда не выбросят исключения, но компилятор не может сам это определить. Должен ли я добавлять
noexcept
к декларации функции во всех таких случаях?Мысль о том, нужно ли добавлять
noexcept
после каждой декларации функции, значительно снижает продуктивность программиста (и, откровенно говоря, это очень утомительно). В каких ситуациях мне следует проявлять бдительность при использованииnoexcept
, а в каких случаях я могу обойтись подразумеваемымnoexcept(false)
? -
Когда я могу реалистично ожидать улучшения производительности после использования
noexcept
? В частности, приведите пример кода, для которого компилятор C++ может сгенерировать более эффективный машинный код после добавленияnoexcept
.Лично мне интересен
noexcept
из-за увеличенной свободы, предоставляемой компилятору, для безопасного применения некоторых видов оптимизаций. Современные компиляторы используютnoexcept
таким образом? Если нет, могу ли я ожидать, что некоторые из них начнут это делать в ближайшем будущем?
5 ответ(ов)
Я думаю, что пока слишком рано давать рекомендации по "лучшему опыту" в этом вопросе, так как не было достаточного времени, чтобы использовать это на практике. Если бы вопрос о спецификаторах исключений был задан сразу после их появления, ответы были бы совершенно другими по сравнению с тем, что мы имеем сейчас.
Нужно постоянно задумываться, нужно ли добавлять
noexcept
после каждого объявления функции, что существенно снизит продуктивность программиста (и, откровенно говоря, будет утомительно).
Ну, тогда используйте его, когда очевидно, что функция никогда не будет выбрасывать исключения.
Когда я могу реально ожидать наблюдать улучшение производительности после использования
noexcept
? [...] Лично для меняnoexcept
важен из-за того, что он дает компилятору больше свободы для безопасного применения определенных видов оптимизаций.
Судя по всему, наибольшие выигрыши в оптимизации происходят за счет пользовательских оптимизаций, а не оптимизаций компилятора, благодаря возможности проверки noexcept
и перегрузки по нему. Большинство компиляторов следуют принципу, что если вы не выбрасываете исключения, то потерь нет, поэтому я сомневаюсь, что это как-то изменит скомпилированный код, хотя, возможно, и уменьшит размер бинарного файла за счет удаления кода обработки исключений.
Использование noexcept
в четырех основных случаях (конструкторы, операторы присваивания; для деструкторов noexcept
уже применим) скорее всего приведет к наилучшим улучшениям, так как проверки noexcept
довольно распространены в шаблонном коде, таком как контейнеры std
. Например, std::vector
не будет использовать ваш перемещающий конструктор, если он не помечен как noexcept
(или если компилятор не может это вывести иным образом).
Как я не устаю повторять в последние дни: семантика на первом месте.
Добавление noexcept
, noexcept(true)
и noexcept(false)
в первую очередь связано с семантикой. Это лишь косвенно влияет на ряд возможных оптимизаций.
Для программиста, читающего код, наличие noexcept
подобно наличию const
: это помогает лучше понять, что может происходить или нет. Поэтому стоит потратить время на размышления о том, знает ли ваша функция, выбрасывает ли она исключения. На всякий случай напомню, что любое динамическое выделение памяти может привести к выбросу исключений.
Теперь о возможных оптимизациях.
Самые очевидные оптимизации фактически выполняются в библиотеке. C++11 предоставляет ряд трейтов, которые позволяют узнать, является ли функция noexcept
или нет, и реализации стандартной библиотеки будут использовать эти трейты, чтобы отдавать предпочтение операциям noexcept
для пользовательских объектов, с которыми они работают, если это возможно. Например,* семантика перемещения*.
Компилятор может лишь немного оптимизировать обработку исключений, потому что он должен учитывать возможность того, что вы могли соврать. Если функция, помеченная как noexcept
, выбрасывает исключение, то вызывается std::terminate
.
Эти семантики были выбраны по двум причинам:
- немедленно получить выгоду от
noexcept
, даже если зависимости еще не используют его (обратная совместимость) - позволить специфицировать
noexcept
при вызове функций, которые теоретически могут выбрасывать исключения, но не ожидается, что сделают это для заданных аргументов.
Это действительно может иметь (потенциально) огромное значение для оптимизатора в компиляторе. Компиляторы на самом деле обладают этой функциональностью уже много лет через пустое выражение throw()
после определения функции, а также с помощью проприетарных расширений. Я могу вас заверить, что современные компиляторы используют эту информацию для генерации более эффективного кода.
Почти каждая оптимизация в компиляторе использует так называемый "граф потока" функции для того, чтобы рассуждать о том, что является допустимым. Граф потока состоит из так называемых "блоков" функции (участков кода с одним входом и одним выходом) и "рёбер" между блоками, указывающих, куда может перейти выполнение. Ключевое слово noexcept
изменяет граф потока.
Вы задали вопрос о конкретном примере. Рассмотрим следующий код:
void foo(int x) {
try {
bar();
x = 5;
// Другие операции, которые не модифицируют x, но могут выбросить исключение
} catch(...) {
// Не изменяем x
}
baz(x); // Или другое выражение, использующее x
}
Граф потока для этой функции будет отличаться, если bar
помечена как noexcept
(в этом случае невозможно, чтобы выполнение перешло от конца bar
к оператору catch). Когда функция помечена как noexcept
, компилятор уверен, что значение x равно 5 во время выполнения функции baz
— блок, где x=5
, так называем "доминирует" над блоком baz(x)
без ребра от bar()
к оператору catch.
Таким образом, компилятор может выполнить так называемую "пропагandu констант", чтобы сгенерировать более эффективный код. В данном случае, если baz
инлайнится, операторы, использующие x, могут также содержать константы, и то, что раньше выполнялось во время выполнения программы, может быть преобразовано в оценку на этапе компиляции и т.д.
В любом случае, короткий ответ: noexcept
позволяет компилятору создать более жесткий граф потока, который используется для обоснования всех видов общих оптимизаций компилятора. Для компилятора аннотации пользователя подобного рода являются очень полезными. Компилятор попытается разобраться в этом, но обычно не может (функция, о которой идет речь, может находиться в другом объектном файле, недоступном компилятору, или обходным образом использовать какую-то функцию, которая тоже недоступна), или, когда и может, существует какое-то тривиальное исключение, которое может быть выброшено, о котором вы даже не подозреваете, поэтому компилятор не может неявно пометить её как noexcept
(например, выделение памяти может выбросить bad_alloc
).
На самом деле, никогда? Это когда-то? Никогда.
noexcept
предназначен для оптимизаций производительности компилятора так же, как и const
— для оптимизаций производительности компилятора. То есть, почти никогда.
Основное предназначение noexcept
— это дать возможность вам на этапе компиляции определить, может ли функция выбросить исключение. Не забывайте: большинство компиляторов не генерируют специальный код для обработки исключений, если на самом деле ничего не выбрасывается. Поэтому noexcept
не столько дает компилятору подсказки о том, как оптимизировать функцию, сколько помогает вам понять, как использовать функцию.
Шаблоны, такие как move_if_noexcept
, определяют, задан ли конструктор перемещения с помощью noexcept
, и если нет, возвращают const&
вместо &&
. Это способ сказать: перемещай, если это действительно безопасно.
В общем, используйте noexcept
, когда считаете, что это действительно полезно. Некоторые куски кода могут повести себя по-разному, если is_nothrow_constructible
истинно для данного типа. Если вы используете код, который будет так себя вести, смело добавляйте noexcept
к соответствующим конструкторам.
Вкратце: используйте его для конструкторов перемещения и похожих конструкций, но не чувствуйте, что надо чрезмерно злоупотреблять этим.
Когда вы говорите, что "я знаю, что функция никогда не выдаст исключение", вы имеете в виду, что, изучив реализацию функции, вы уверены в этом. Однако такая логика может быть ошибочной.
Лучше рассматривать возможность выброса исключений как часть дизайна функции. Это так же важно, как и список аргументов или то, является ли метод модификатором (... const
). Заявление о том, что "эта функция никогда не выбрасывает исключений", является ограничением для реализации. Его отсутствие не означает, что функция может выбросить исключения, а говорит о том, что текущая версия функции и все ее будущие версии могут выбрасывать исключения. Это ограничение усложняет реализацию. Но некоторые методы должны иметь такое ограничение, чтобы быть практически полезными; особенно это важно для того, чтобы их можно было вызывать из деструкторов, а также для реализации "откатного" кода в методах, которые обеспечивают сильную гарантию исключений.
Почему следует использовать указатель вместо самого объекта?
Что такое std::move() и когда его следует использовать?
Какова разница между 'typedef' и 'using'?
В чем разница между constexpr и const?
Покончены ли дни передачи const std::string & в качестве параметра?