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

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

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

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

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

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

10 ответов

Решение

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

Нужно думать о том, нужно ли мне добавить 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;
        // 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 и заявление улова). Когда помечены как noexceptкомпилятор уверен, что значение x во время функции baz равно 5 - говорят, что блок x=5 "доминирует" над блоком baz(x) без ребра из bar() к заявлению поймать. Затем он может сделать то, что называется "постоянное распространение", чтобы генерировать более эффективный код. Здесь, если baz встроен, операторы, использующие x, могут также содержать константы, а затем то, что раньше было оценкой во время выполнения, может быть превращено в оценку во время компиляции и т. Д.

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

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

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

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

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

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

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

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

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

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

Там, где завершение является приемлемым ответом, необработанное исключение будет достигать этого, потому что оно превращается в вызов 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);      
     // ...   
 }

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

  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: Объявите функции без исключения, если они не будут генерировать исключения" для дальнейшего чтения.

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

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

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

Вот простой пример, чтобы проиллюстрировать, когда это действительно может иметь значение.

      #include <iostream>
#include <vector>
using namespace std;
class A{
 public:
  A(int){cout << "A(int)" << endl;}
  A(const A&){cout << "A(const A&)" << endl;}
  A(const A&&) noexcept {cout << "A(const A&&)" << endl;}
  ~A(){cout << "~S()" << endl;}
};
int main() {
  vector<A> a;
  cout << a.capacity() << endl;
  a.emplace_back(1);
  cout << a.capacity() << endl;
  a.emplace_back(2);
  cout << a.capacity() << endl;
  return 0;
}

Вот результат

      0
A(int)
1
A(int)
A(const A&&)
~S()
2
~S()
~S()

Если мы удалим noexcept в конструкторе перемещения, вот результат

      0
A(int)
1
A(int)
A(const A&)
~S()
2
~S()
~S()

Ключевое отличие - vs A(const A&&). Во втором случае он должен скопировать все значения с помощью конструктора копирования. ОЧЕНЬ НЕЭФФЕКТИВНЫЙ !!

Учитывая тот факт, что количество правил для работы с C++ постоянно увеличивается, и что мы жили счастливо без каких-либо исключений в течение многих лет... Я думаю, что если кто-то не дает веских причин, почему его использовать никто не будет... или, по крайней мере, каждый день с ++ Разработчики

Другие вопросы по тегам