четверг, 19 декабря 2013 г.

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



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

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


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



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

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

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

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






Те кто много работают с 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.




Представьте себе ситуацию: вам нужно обменять значения двух переменных наиболее эффективным образом. Обычно для этого используют функции 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 и др.




Начнем с достаточно тривиального совета: если вы знаете количество элементов, которые будут вставлены в вектор, то перед вставкой необходимо вызвать 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.




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

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


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




В книге имеется много интересного по С++ и Boost, рассказывать можно долго. Что бы вы хотели услышать в первую очередь:

This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий