Как стать автором
Обновить

Комментарии 15

Хорошая статья, но:
Главный вывод — ставьте noexcept везде. Это просто одно слово, написание которого ничего не стоит, но при этом вы получаете потенциальный выигрыш с точки зрения производительности ваших приложений.

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

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

Ставьте noexcept там, где исключений нет и не будет по дизайну (например, в move конструкторах & операторах, обертках над C-кодом и т.п.), а не там, где их нет сегодня, потому что звёзды так сложились.
Вы же сами выше пишете:

Ну так это аргумент противоположного лагеря, лагеря «не ставить везде noexcept».

Основной посыл в том, что нетровающих методов достаточно много, может даже больше, чем тровающих. С++ вообще славится неправильными дефолтами — надо не пинать людей которые explicit у конструкторов не пишут, надо было делать их explicit по дефолту.

К примеру, если у вас геттер возращает std::string_view (или const-ref), то он, скорее всего, noexcept. Даже если вы захотите потом возвращать std::string как-то хитро посчитанный, сигнатура метода (и бинарная совместимость, если она вас заботит) уже будет сломана и наличие noexcept тут врядли будет самой большой проблемой.
Если же вы не разрабатываете библиотеку и вам не важны такие вещи, то пишите noexcept везде — потом можно снять и просто пересобрать код. Шанс того, что кто-то будет делать оптимизации наподобие тех что делает std::vector, проверяя is_nothrow_move_constructible для вашего геттера/алгоритма, исчезающе мал.
Ну типа да, может быть ситуация когда люди увидят noexcept и вместо unique_ptr где-нибудь заюзают delete (ведь так быстрее! и ислючения не тровают, вот же noexcept!) и потом, когда noexcept уберут, и в этом коде пролетит исключение, объект утечет… Ну так не надо заниматься premature optimisation и закладываться на это, пока профайлер не скажет что «вот это RAII тут тормозит».

Основная и главная проблема noexcept в том, что нет compile-time проверки действительно ли оно noexcept. Забыв поймать где-то исключение — получаешь риск std::terminate на пустом месте!


Соответственно единственный ТруЪ-способ использовать noexcept это писать что-то такое:


#include <iostream>

#define NOEXCEPT(EXPR) ([&]() { static_assert(noexcept(EXPR), "Expression can throw"); }, EXPR)

auto fun() noexcept 
{
    auto x = NOEXCEPT(int(3));
    //auto x = NOEXCEPT(std::string("3")); //won't compile
    return x;
}

int main()
{
    std::cout << fun() << std::endl;
}

Но как можно понять, компилироваться такое будет очень долго.

Но как можно понять, компилироваться такое будет очень долго.

Мне кажется, это в первую очередь не очень читаемо, тормознутость — отдельная проблема.

Основная и главная проблема noexcept в том, что нет compile-time проверки действительно ли оно noexcept.

Правда ваша, компилятор мог бы форсить noexcept как это делается для const.
Тем не менее, в clang-tidy имеется проверка bugprone-exception-escape, которая, судя по документации, как раз призвана ловить случаи, когда кидается исключение там где его не должно быть. Если у кого есть опыт использования этой проверки — поделитесь, пожалуйста.
Спасибо за фидбэк. Все правильно говорите: noexcept — это часть интерфейса (и на последнем слайде, где главный вывод, упомянуто, что нужно его ставить с осторожностью).

А насчет изменения ситуации, то это как если бы вы сделали метод константным (что тоже, конечно, часть интерфейса), а потом, например, поняли, что это hot path и что ради увеличения производительности нужно кешировать какие-то значения в этом методе, то для этого вероятно объявите кеш как mutable, но не снимите константости с интерфейса (т.к. в этом случае нарушите интерфейс).

Аналогично и здесь: если при начальном дизайне ваш метод помечен как noexcept, а потом, в силу implementation details причин, вы решаете вызвать из этой функции (помеченной noexcept) функцию, бросающую исключение, то делаете try/catch внутри самой функции, но функция при этом остается noexcept. А если же изначальный дизайн по каким-то причинам меняется принципиально, например, полностью переделывается схема обработки ошибок, то, думаю, в таком случае снятие с функций noexcept будет далеко не главной проблемой.

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

Данный доклад не преследует цель пропаганды отказа от использования исключений. Он нацелен показать, что относиться к простановке noexcet следует более внимательно.

Ставьте noexcept там, где исключений нет и не будет по дизайну (например, в move конструкторах & операторах, обертках над C-кодом и т.п.), а не там, где их нет сегодня, потому что звёзды так сложились

Согласен: именно если так по дизайну, а не сегодня так, завтра по-другому. Единственное дополню, что это могут быть не только обертки и move конструкторы, но также и многое другое, если это четко прописано в интерфейсе функции (например, что функция возвращает std::optional)
потом можно снять и просто пересобрать код
Снятие, увы, подразумевает ручной анализ всего графа вызовов, т.к. если вы снимете не везде, то получится не compile time error, a terminate в рантайме.

может быть ситуация когда люди увидят noexcept и вместо unique_ptr где-нибудь заюзают delete… и потом, когда noexcept уберут, и в этом коде пролетит исключение, объект утечет
Скорее «люди увидят noexcept и пометят свой код, вызывающий ваш код, тоже как noexcept (т.к. они читали хабр и знают, что это хорошо), а когда в один прекрасный день ваш код перестанет быть noexcept и из него прилетит исключение — всё просто накроется медным тазом.
Снятие, увы, подразумевает ручной анализ всего графа вызовов, т.к. если вы снимете не везде, то получится не compile time error, a terminate в рантайме.


Почему? Только если вы продолжаете навешивать noexcept выше по стеку как в вашем примере ниже.

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

Это валидный поинт и так делать надо далеко не всегда — если функция достаточно сложная или использует другие функции, о содержимом которых нельзя делать предположения (например, внешняя библиотека), то noexcept выше по стеку навешивать не надо. Никто и не предлагает, вообще-то.
По сути, разговор идет о значениях по умолчанию — по дефолту функцию надо делать constexpr noexcept const (ничего не забыл?) если имплементация позволяет.
Выше по стеку это не предлагается распространять, особенно если у вас там начинается бизнес-логика.
Проблема как раз в том, что из-за вышеназванной аргументации noexcept не пишут вообще нигде, даже в тех тривиальных случаях, когда имплементация метода не позволяет что-либо выбросить никогда в будущем (скажем, to_string от енума, возвращающий std::string_view от данных в RO секции).
noexcept выше по стеку навешивать не надо. Никто и не предлагает, вообще-то

Как же никто не предлагает? Автор вон прямым текстом рекомендует «ставьте noexcept везде». А «везде» — это таки везде, в том числе и выше по стеку.
Поинт как раз в том, что возводить в абсолют не стоит, стоит думать, в том числе и о возможных последствиях.
Как я писал выше, «везде» предполагает — исходно по дизайну, а не везде без разбора.

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

Компилятор все увидит. Если у вас в объявлении функции прописан noexcept, то компилятор будет на это закладываться, даже если определение функции будет в другом translation unit.

Хочется чтобы это было автоматически. Наверное проверить ловит ли catch все или нет компилятору будет cложно. Но хотя бы для функций в которых ниже по стеку не бросаются исключения или явно помечено noexcept — компилятор может обойти все рекурсивно и вывести этот факт (при условии доступа к телу или что предлагаю я — некую метаинформацию о теле функций).

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

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

Да неужели? Inline!
Зарегистрируйтесь на Хабре, чтобы оставить комментарий