Спецификатор времени компиляции noexcept в C++11

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

Уменьшение размера бинарного файла


Давайте рассмотрим следующий пример, в котором комментариями рассказывается, что за код генерируется компилятором:
#include <iostream>

class my_class {
    int i_;

public:
    explicit my_class (int i)
        : i_(i)
    {}

    int get() const {
        return i_;
    }
};

inline int operator+(const my_class& v1, const my_class& v2) {
    return v1.get() + v2.get();
}

int main() {
    int res = 0;
    try {
        // Если при вызове конструктора для var0 произойдет исключение,
        // распечатываем сообщение об ошибке и выставляем res в -1
        my_class var0(10);

        // Если при вызове конструктора для var1 произойдет исключение,
        // то компилятор должен будет вызвать деструктор
        // переменной var0, распечатать сообщение об ошибке
        // и выставить res в -1
        my_class var1(100);

        // Если при сложении произошло исключение,
        // распечатываем сообщение об ошибке и выставляем res в -1
       
        // Вызов деструкторов var1 и var0 произойдет в любом случае,
        // вне зависимости от генерации исключения.
        res = (var1 + var0 > 0 ? 0 : 1);
    } catch (...) {
        std::cerr << "Произошла ошибка";
        res = -1;
    }

    return res;
}

Многовато кода генерируется компилятором, не правда ли? Именно из-за такого разбухания кода, в некоторых крупных корпорациях (не будем тыкать пальцем в Google) при разработке на С++ запрещено использование исключений. Еще одним примером могут послужить правила разработки для GCC начиная с версии 4.8 (да, GCC теперь разрабатывается с использованием С++, см изменения для 4.8).

Давайте модифицируем класс my_class, чтобы он использовал noexcept и посмотрим что может генерировать компилятор:
class my_class {
    int i_;

public:
    explicit my_class (int i) noexcept
        : i_(i)
    {}

    int get() const noexcept {
        return i_;
    }
};

inline int operator+(const my_class& v1, const my_class& v2) noexcept {
    return v1.get() + v2.get();
}

int main() {
    // std::terminate при исключении
    my_class var0(10);

    // std::terminate при исключении
    my_class var1(100);
       
    // std::terminate при исключении
    int res = (var1 + var0 > 0 ? 0 : 1);

    // Вызов деструкторов var1 и var0
    return res;
}


Заметьте, что std::cerr больше не используется, а следовательно компилятор сможет убрать очень большой кусок кода. Но даже без этого, количество сгенерированного кода стало меньше.

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

Вот результаты компиляции различных версий одного класса на GCC 4.7.2:
1) Первоначальный класс, тело main из первого варианта, флаги -std=c++0x -fno-inline -O2: 5280 байт
2) Класс оптимизирований, тело main из второго варианта, флаги -std=c++0x -fno-inline -O2: 4800 байт

3) Класс оптимизирований, тело main из первого варианта, флаги -std=c++0x -fno-inline -O2: 5280 байт
4) Класс оптимизирований, тело main из первого варианта без iostream, флаги -std=c++0x -fno-inline -O2: 4800 байт
5) Класс оптимизирований, тело main из второго варианта + iostream включается, флаги -std=c++0x -fno-inline -O2: 5280 байт

* Флаг -std=c++0x включает возможности C++11, который необходим для использования noexcept.
** При повторении эксперимента, не забудьте убрать отладочную информацию из бинарного файла.

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

Немного схитрим, и разнесем метод main() и имплементацию my_class по разным файлам, чтобы компилятору было сложнее оптимизировать.
Результаты:
Первоначальный класс, тело main из первого варианта, флаги -std=c++0x -fno-inline -O2: 6952 байт
Класс оптимизирований, тело main из первого варианта, флаги -std=c++0x -fno-inline -O2: 5288 байт

Итого: выигрыш в размере в 23.9% после добавления трех noexcept.

Ускорение работы стандартных алгоритмов


Для многих людей, использующих С++11 может стать большой неожиданностью данная часть стандарта N3050 (на данный момент этот стандарт реализован не во всех ведущих компиляторах). Если в кратце, в ней говорится, что стандартные алгоритмы и контейнеры не должны использовать move assignment и move construction если эти методы могут кидать исключения. Чем это может грозить рассмотрим на примере:
// Класс с ресурсоёмким копированием и быстрым move assignment
class move_fast_copy_slow {
    // Некие члены класса объявлены здесь
public:
    move_fast_copy_slow(move_fast_copy_slow&&);         // 1
    move_fast_copy_slow(const move_fast_copy_slow&);    // 2
   
    move_fast_copy_slow& operator=(move_fast_copy_slow&&);      // 3
    move_fast_copy_slow& operator=(const move_fast_copy_slow&); // 4
};


Если методы 1 и 3 не пометить noexcept, то стандартные контейнеры будут использовать более медленные методы 2 и 4. Вследствие этого работа с контейнерами std::vector и std::deque может замедлиться на пару порядоков. При том, данное замедление коснется и всех типов наследуемых или использующих move_fast_copy_slow в качестве члена.

Совместимость


На момент написания данной статьи не все ведущие компиляторы поддерживают noexcept. Используйте макрос BOOST_NOEXCEPT из библиотеки boost вместо noexcept для переносимости кода.

Подводные камни


По стандарту, noexcept не является частью типа функции, однако при использовании виртуальных функций все перегруженные функции должны иметь такую же либо более строгую спецификацию исключений. То есть следующий код не соберется:
class base {
public:
    virtual void foo() noexcept {}
};

class derived: public base {
public:
    void foo() override { throw 1; }
};

int main () {
    derived d;
    base *p = &d;
    p->foo();
}

Будет ошибка типа «error: looser throw specifier for ‘virtual void derived::foo()’».
Так что будьте готовы к тому, что если вы автор библиотеки и добавили спецификацию noexcept к фиртуальной функции, то у пользователей вашей библиотеки перестанет собираться код.
Так же приготовьтесь к тому, что ваши классы исключений c перегруженным методом what() и наследуемые от стандартных, могут не компилироваться если они не помечены noexcept:
class my_exception : public std::exception {
    const char* what() throw() { // В С++11 должно быть noexcept вместо throw()
        return "my_exception::what()";
    }
};

Заключение


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

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 17

    –5
    Это не новая возможность C++11, в С++03 можно было использовать throw().
      0
      Фундаментально разные вещи
        +2
        И в чем же их фундаментальное отличие?

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

        Есть отличие заключающиеся в том что noexcept может принимать аргумент типа bool, которое может быть очень полезна совместно с ОПЕРАТОРОМ noexcept, но это отличие не связано напрямую с оптимизацией про которую писал автор.
          +3
          Сами же ответили на свой вопрос. Эти два отличия и есть фундаментальная разница.
            +1
            Это не фундаментальные отличия, noexcept можно рассматривать как несколько усовершенствованную версию throw() но не как что-то кардинально новое как об этом рассказывает автор. Если глянуть на стандартную библиотеку C++11 то там, где используется noexcept раньше использовался throw().
              +1
              Ага, с таким же успехом операторы new/delete можно рассматривать как «несколько усовершенствованную » версию malloc/free, ведь они «всего-лишь» вызывают конструктор/деструктор в дополнение к (де)аллокации памяти.

              необязятальность раскрутки стэка в случае с noexcept — это фундаментальная разница. Т.к. в деструкторах может быть могого интересных вещей типа удаления временных файлов, протокольное закрытие соединений итд итп. Поэтому прострая замена throw() на noexcept скорее всего не сработает во многих проектах.
                +1
                1. По моему ваше сравнение не очень корректно: new выполняет дополнительную задачу: конструирование объекта, которая не менее важна чем выделение памяти. noexcept не выполняет дополнительной работы в сравнении с throw(), у него та же самая задача.

                2. Необязательность раскрутки стэка в случае с noexcept это всего лишь оптимизация, в любом случае продолжать работу после нарушения спецификатора исключений бессмысленно поскольку это аварийная ситуация, и разница в поведении не имеет особенного значения, поэтому для noexcept оно не и определено.
      +5
      Еще стоит добавить, что кроме спецификатора noexcept есть еще и оператор noexcept, который на этапе компиляции может вычислить, можно применить спецификатор noexcept или нет. Это очень полезно для шаблонов

      template <class A> 
      void swap (A& a, A& b) noexcept (noexcept(A::operator=))
      {
          A tmp = a;
          a = b;
          b = tmp;
      }
      


      Здесь спецификатор noexcept будет включен только если A::operator= и все что он вызывает, тоже имеют спецификатор noexcept

      «Благодяря» этому оператору возникла некоторая путаница в рядах с++ программистов — многие решили что спецификатор noexcept тоже выполняет проверку на этапе коипиляции. Однако, как вы правильно указали в конце статьи, это не так.
        0
        В boost постарались решить путаницу, введя макросы BOOST_NOEXCEPT, BOOST_NOEXCEPT_IF(предикат)
        BOOST_NOEXCEPT_EXPR(выражение).

        Если их использовать, приведенный выше пример быдет выглядеть так:

        template <class A> 
        void swap (A& a, A& b) BOOST_NOEXCEPT_IF(BOOST_NOEXCEPT_EXPR(A::operator=))
        {
            A tmp = a;
            a = b;
            b = tmp;
        }
        

        Выглядит чуточку понятнее (и немного более громоздко).

        Если решите использовать оператор noexcept, стоит отметить, что компилятор GCC 4.7 достаточно часто падает с Internal Compiler Error при использовании сложных выражений.
        0
        Многовато кода генерируется компилятором, не правда ли? Именно из-за такого разбухания кода, в некоторых крупных корпорациях (не будем тыкать пальцем в Google) при разработке на С++ запрещено использование исключений. Еще одним примером могут послужить правила разработки для GCC начиная с версии 4.8 (да, GCC теперь разрабатывается с использованием С++, см изменения для 4.8).

        В этом Google C++ Style Guide пункт про размер бинарника идёт четвёртым из пяти аргументов против, а в GCC Coding Conventions вовсе отсутствует.
        Очевидно, разбухание кода не является столь важной проблемой для корпораций, как это подано в статье. Кроме того, эксепшены и весь сопутствующий механизм ориентированы на минимизацию оверхеда в случае, когда они не бросаются.
          0
          В этом Google C++ Style Guide пункт про размер бинарника идёт четвёртым из пяти аргументов против
          noexcept так же служит и документацией к функциям (пункт второй из Google C++ Style Guide), так что уже соотношение пунктов «за» и «против» не 5:5 а где-то 5:3

          эксепшены и весь сопутствующий механизм ориентированы на минимизацию оверхеда в случае, когда они не бросаются
          — 100% верно для C++03, отчасти верно для C++11 в котором есть rvalue references (деструктивное копирование) и в котором уже алгоритмы подстраиваются в зависимости от спецификаторов исключений. См. «Ускорение работы стандартных алгоритмов»
          +1
          Были времена, когда я не боялся кода на Си++. Но с приходом в мир нового Стандарта, что-то как-то мне всё больше не по себе. Неужели инженеры не понимают, что сложность использования языка растёт экспоненциально от количества базовых сущностей в нём? Хм… А, главное, зачем? Неужели компилятор сам не может понять, что в функции не будут выбрасываться исключения? Критерии же, если не очевидны, то ясны.
            +1
            Нет, не может, если функция библиотечная, то компилятор вообще не особо может быть в курсе что у неё в теле содержится. Вот если бы в С++ были нормальные модули, то тогда бы компилятор сам мог генерировать всю дополнительную метаинформацию о функциях. И то noexcect пришлось бы вводить для точной настройки.
              0
              Если функция библиотечная (динамическая линковка), то и программист не может написать для неё noexcept (ибо, кто его знает, откуда оно там выстрелит). Если же она попадает под возможности link-time оптимизации, то информации достаточно, чтобы вывести отсутствие исключений.
                0
                Может написать разработчик библиотеки в заголовочном файле.
                  –4
                  Эх… Может, конечно. Но зачем? Это же будет, как с const. Добавляем в интерфейс const, и всё, везде придётся писать этот const. Компилятор должен сам такие вещи выводить, ну что, у нас Haskell-а, что ли нет и не развита теория типов? Зачем C++ вести по пути COBOL-а?
            0
            Вы пишите: «Так же приготовьтесь к тому, что ваши классы исключений c перегруженным методом what() и наследуемые от стандартных, могут не компилироваться если они не помечены noexcept».
            Я бы с вами поспорил, если бы вы написали «не должны компилироваться», но вы пишите «могут не компилироваться», поэтому спорить с вами я не буду. Вместо этого уточню некоторые моменты.
            1. Динамические спецификации исключений (формы throw) являются запрещенными в С++11. Это означает, что теперь компиляторы могут выдавать предупреждения (но не ошибки) при их использовании. Однако я пока не работал с компиляторами, которые бы это делали (gcc 4.7.2, clang 3.2 молчат даже в режимах максимальной педантичности).
            2. Спецификация throw() является совместимой с noexcept и noexcept(true) (пункт 15.4/3 стандарта). А это означает, что приведенный вами пример должен компилироваться. Более того, в вашем примере ограничений еще меньше, поскольку речь идет о переопределении функции (смотрите 15.4/5 стандарта). В принципе эта совместимость также означает, что оператор noexcept должен работать корректно и функциями со спецификацией throw() (пункт 5.3.7/3 стандарта).
            На что действительно компиляторы могут выдавать предупреждение в вашем примере, так это на отсутствие квалификатора const у функции what().

            Only users with full accounts can post comments. Log in, please.