Зачем что-то обещать или почему noexcept меняет поведение кода?
Если бы по телевизору показывали рекламу языка C++, наверняка, в ней бы было что-то вроде:
«С добавлением move-семантики вы получаете бесплатное увеличение производительности за счёт избавления от нецелесообразных операций копирования. Просто перекомпилируйте свой проект новой версией компилятора с поддержкой 11 или 14 стандарта и радуйтесь ускорению работы программы!».
Стоит ли верить такой рекламе?
Компилятор действительно может сгенерировать конструктор перемещения и оператор перемещающего присваивания. Это поможет избежать некоторых избыточных копирований. Однако в некоторых ситуациях компилятор не сможет выполнить генерацию. Перемещающие операции генерируются только в случае необходимости и если соблюдены следующие условия:
- В классе не объявлены никакие пользовательские копирующие операции (никаких конструкторов копирования и никаких операторов присваивания);
- В классе не объявлены никакие пользовательские операции перемещения (никаких конструкторов перемещения и никаких операторов перемещения);
- В классе не объявлен пользовательский деструктор.
Допустим, мы не сможем удовлетворить какое-нибудь из этих требований, и нам придётся реализовать, скажем, пользовательские операции копирования. Очевидно, тогда придётся определить и пользовательские операции перемещения.
Получим ли мы автоматическое ускорение старого кода, как обещано в рекламе?
Предположим, мы взяли старый код и добавили к уже существующему (и используемому) классу операции копирования и перемещения. В итоге получилось что-то вроде следующего:
struct SomeClass { SomeClass(); ~SomeClass(); SomeClass(const SomeClass&); SomeClass(SomeClass&&); SomeClass& operator=(const SomeClass&); SomeClass& operator=(SomeClass&&); };
Если приглядеться, то можно заметить, что мы забыли указать спецификатор noexcept для перемещающих операций (либо не забыли, а не смогли реализовать их безопасно с точки зрения исключений).
К чему это приведёт?
К тому, что мы будем сильно недовольны рекламой, ожидая везде, где только это возможно, перемещение, и получая копирование в некоторых случаях. Немного запутанно, но сейчас станет чуть понятнее.
Рассчитывать на перемещение мы сможем только в тех случаях, когда в коде присутствует явная работа с r-value. Например, вот так:
А разве есть другие?
Да, есть и другие. И эти другие зачастую являются более желанными, чем явные случаи использовать перемещения. Примерами таких неявных случаев использования перемещения являются: алгоритмы, внутренние реалокации памяти с дальнейшим перемещением объектов. Если перемещающие операторы не помечены спецификатором noexcept, компилятор предпочтёт не рисковать и выберет копирующие операции.
Вывод
Использование спецификатора noexcept (или наоборот его неиспользование) может серьёзно изменить производительность вашей программы.
Есть вопросы? Напишите в комментариях!