С новым стандартом С++ появилось множество интересных и полезных улучшений, одно из которых спецификатор времени компиляции noexcept, которой говорит компилятору о том, что функция не будет выбрасывать исключения. Если интересно, какие преимущества предоставляет этот спецификатор и не пугает код на С++ — добро пожаловать под кат.
Давайте рассмотрим следующий пример, в котором комментариями рассказывается, что за код генерируется компилятором:
Многовато кода генерируется компилятором, не правда ли? Именно из-за такого разбухания кода, в некоторых крупных корпорациях (не будем тыкать пальцем в Google) при разработке на С++ запрещено использование исключений. Еще одним примером могут послужить правила разработки для GCC начиная с версии 4.8 (да, GCC теперь разрабатывается с использованием С++, см изменения для 4.8).
Давайте модифицируем класс my_class, чтобы он использовал noexcept и посмотрим что может генерировать компилятор:
Заметьте, что 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 если эти методы могут кидать исключения. Чем это может грозить рассмотрим на примере:
Если методы 1 и 3 не пометить noexcept, то стандартные контейнеры будут использовать более медленные методы 2 и 4. Вследствие этого работа с контейнерами std::vector и std::deque может замедлиться на пару порядоков. При том, данное замедление коснется и всех типов наследуемых или использующих move_fast_copy_slow в качестве члена.
На момент написания данной статьи не все ведущие компиляторы поддерживают noexcept. Используйте макрос BOOST_NOEXCEPT из библиотеки boost вместо noexcept для переносимости кода.
По стандарту, noexcept не является частью типа функции, однако при использовании виртуальных ф��нкций все перегруженные функции должны иметь такую же либо более строгую спецификацию исключений. То есть следующий код не соберется:
Будет ошибка типа «error: looser throw specifier for ‘virtual void derived::foo()’».
Так что будьте готовы к тому, что если вы автор библиотеки и добавили спецификацию noexcept к фиртуальной функции, то у пользователей вашей библиотеки перестанет собираться код.
Так же приготовьтесь к тому, что ваши классы исключений c перегруженным методом what() и наследуемые от стандартных, могут не компилироваться если они не помечены noexcept:
Спецификатор времени компиляции noexcept сильно уменьшает размер итогового файла и ускоряет работу программы. Главное при использовании noexcept не переусердствовать. Помните, что если функция помеченная noexcept выпустит исключение наружу, то ваша программа вызовет std::terminate() и завершится, даже не соблаговолив вызвать деструкторы для уже созданных переменных.
Уменьшение размера бинарного файла
Давайте рассмотрим следующий пример, в котором комментариями рассказывается, что за код генерируется компилятором:
#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() и завершится, даже не соблаговолив вызвать деструкторы для уже созданных переменных.