Pull to refresh

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

Reading time5 min
Views67K
С новым стандартом С++ появилось множество интересных и полезных улучшений, одно из которых спецификатор времени компиляции 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() и завершится, даже не соблаговолив вызвать деструкторы для уже созданных переменных.
Tags:
Hubs:
+23
Comments17

Articles