Подтвердить что ты не робот

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

Ответ 2

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

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

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


Хорошо, теперь перейдем к возможной оптимизации.

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

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

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

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

Ответ 3

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

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

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

void foo(int x) {
    try {
        bar();
        x = 5;
        // Other stuff which does not 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)

Ответ 4

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

Ответ 5

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

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

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

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

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

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

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

Ответ 6

  • Есть много примеров функций, которые, как я знаю, никогда не будут бросать, но для которых компилятор не может определить это самостоятельно. Должен ли я добавить 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, если они не будут испускать исключения" для дальнейшего чтения.

Ответ 7

In Бьярне словами (язык программирования 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);
    // ...
}

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

Ответ 8

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

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

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