Pull to refresh

C++ трюки и советы из Boost на каждый день

Reading time 3 min
Views 42K

В недавнем посте хабровчане проголосовали за то, чтобы главы из книги были переведены на русский. Немного подумав я решил схитрить, и вместо перевода глав из открытого доступа, рассказать о том, что есть в закрытых.

Дабы не нарушать прав издательства, это будет не дословный перевод, а выжимка из тех техник, что могут показаться интересными даже людям не работающим с Boost.

Что вас ждёт под катом:

  • Избегаем вызовов макросов вместо функций, на примере max/min.
  • Вызываем оптимальную функцию, на примере std::swap и её специализации в разных пространствах имен.
  • Ускоряем вставку в std::vector.
  • Деструкторы в C++11.



Избегаем вызовов макросов вместо функций, на примере max/min.


Те кто много работают с Visual Studio наверняка сталкивались с тем, что min/max — это макросы. Из-за чего практически любые функции min/max в любых классах и пространствах имен трактуются препроцессором как макроподстановки. В следствие этого следующие примеры кода не компилируются:

int max_int = std::numeric_limits<int>::max();
my_class_variable.max();


Есть различные способы избежать вызовов макросов вместо функций, но самый переносимый и короткий — это просто заключить весь вызов функции в круглые скобки (кроме самих круглых скобок):

int max_int = (std::numeric_limits<int>::max)();
(my_class_variable.max)();


Теперь препроцессор не воспримет max как вызов макроса и код скомпилируется верно. Этот трюк срабатывает со всеми макросами, не только с min/max, и часто используется в Boost.

Вызываем оптимальную функцию, на примере std::swap и её специализации в разных пространствах имен.


Представьте себе ситуацию: вам нужно обменять значения двух переменных наиболее эффективным образом. Обычно для этого используют функции swap. Но вот беда, мы пишем обобщенный код который должен работать с пользовательскими типами:
template <class T>
void my_function(T& value1, T& value1) {
    // ...
    std::swap(value1, value2); // не оптимальное решение!
    // ...
}

Неоптимальность решения заключается в том, что пользовательские типы могут иметь свою функцию swap, которая работает намного эффективнее стандартной версии:
namespace some_namespace {
    class my_vector;
    void swap(my_vector& value1, my_vector& value2);
} // some_namespace

Очень простым решением проблемы будет исправить код следующим образом:
template <class T>
void my_function(T& value1, T& value1) {
    using std::swap;
    // ...
    swap(value1, value2);
    // ...
}

Теперь компилятор в первую очередь будет пытаться найти функцию swap из пространств имен параметров value1 и value2. Другими словами, если value1 и value2 являются экземплярами класса some_namespace::my_vector, то компилятор в первую очередь будет пытаться использовать swap из some_namespace. Если в пространствах имен параметров value1 и value2 не будет функции swap, компилятор будет использовать std::swap.

Почему происходит именно так? Потому что компилятор должен выполнить Argument Dependent Lookup (он же Koenig Lookup) прежде чем пытаться сделать вызов функции из using. Именно этим трюком пользуется boost::swap, а boost::numeric_cast использует аналогичный подход для функций floor, ceil и др.

Ускоряем вставку в std::vector.


Начнем с достаточно тривиального совета: если вы знаете количество элементов, которые будут вставлены в вектор, то перед вставкой необходимо вызвать reserve(количество элементов для вставки):
std::vector<int> numbers;
numbers.reserve(1000);
for (size_t i = 0; i < 1000; ++i)
    numbers.push.back(i);
// ...

Этот совет имеется во множестве учебников по программированию, но почему-то о нем часто забывают. Из недавних примеров где об этом забыли — код Unity (графической оболочки Ubuntu).

Теперь менее тривиальный совет. В C++11 было решено, что std::vector и ряд других контейнеров могут использовать move-конструкторы и move-assignment операторы для элементов только если move-конструкторы и move-assignment операторы этих элементов не кидают исключений (ну или если элементы не могут копироваться).

Другими словами, в C++11 для достижения наилучшей производительности стоит помечать конструкторы и операторы, не кидающие исключения, как noexcept.

Помимо контейнеров из стандартной библиотеки, многие классы из Boost чувствительны к noexcept, например boost::variant и boost::circular_buffer.

Деструкторы в C++11.


Ещё до стандарта С++11 кидать исключения в деструкторах считалось очень плохим тоном. В C++11 это ведет к смерти приложения.

В С++11 все деструкторы объектов автоматически помечаются как noexcept. Это ведет к тому, что если у вас есть класс, который кидает исключение в деструкторе, то в C++11 в этом случае будет вызван std::terminate() и всё приложение завершится без вызова деструкторов для созданных объектов.

Конечно можно это обойти, явно сказать компилятору что деструктор кидает исключение… Но не лучше ли сделать все по хорошему?

Вместо итогов


В книге имеется много интересного по С++ и Boost, рассказывать можно долго. Что бы вы хотели услышать в первую очередь:
Only registered users can participate in poll. Log in, please.
В следующей статье вы бы хотели узнать больше о…
69.64% … С++11/C++14 и Boost 312
48.88% … тонкостях при работе с Boost и о том, чего нет в документации 219
58.93% … системном программировании (быстрее работаем с файлами, более быстрые conditional variables) 264
448 users voted. 61 users abstained.
Tags:
Hubs:
+31
Comments 15
Comments Comments 15

Articles