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

  • Tutorial

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

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

В следующей статье вы бы хотели узнать больше о…

  • 69.6%… С++11/C++14 и Boost312
  • 48.8%… тонкостях при работе с Boost и о том, чего нет в документации219
  • 58.9%… системном программировании (быстрее работаем с файлами, более быстрые conditional variables)264
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 15

    +1
    Про min и max. Пишем
    #define NOMINMAX
    и эти макросы не определены.
      +3
      Да, именно эти макросы будут не определены (другие макросы вашим методом не обойти).
      + нельзя так писать в заголовочных файлах библиотеки, которая будет использоваться третьими лицами — это может поломать их код.
        0
        Да, я понимаю это. Но вообще такие проблемы правильнее решать на уровне «Coding Style», и серьезно наказывать за отклонения от него.
          +1
          Уже позно, MS много лет назад определила кучу макросов в WinAPI.
          +1
          + нельзя так писать в заголовочных файлах библиотеки, которая будет использоваться третьими лицами — это может поломать их код.

          Отмечу, что при использовании cl для обхода этого момента можно использовать:
          #pragma push_macro("min")
          #undef min
          // a lot of code
          #pragma pop_macro("min")
          
          для gcc и подобных компиляторов есть аналоги (наизусть не помню, там обычно таких проблем не возникает).

          Но вариант со скобками мне нравиться гораздо больше чем эти непереносимые решения.
            0
            В заглавии статьи указан С++, в нет нет макросов min и max, равно как и макросов __min и __max. Это вольности microsoft. Другими словами, такие проблемы являются проблемами стиля программирования, а не проблемами совместимости. Собственно это я имел в виду.
            Есть кстати еще пример о пользе скобочек при использовании макросов:
            #define SOME_MACRO(x)	(x)
            
            template<typename T1, typename T2>
            T2 some_func(T1 x)
            {
            	return x;
            }
            
            int main()
            {
            	SOME_MACRO(1 == (some_func<int,int>(1)));
            
            	return 0;
            }

            Без скобочек вокруг вызова функции даже не компилируется.
          +1
          Так и описано, что это касается не только MIN и MAX, но и других макросов, а в данном случае описана просто техника.
          +7
          Извините, но разве это трюки/советы? Ладно, с макросами — трюк, но vector::reserve, swap — это ведь и не трюки, и не советы. И статья на статью не тянет.
          Извините ещё раз.
            +3
            Хочу поблагодарить автора за книгу по Boost. Книг по этой библиотеке вообще крайне мало (по бусту в целом всего 3 штуки, включая сабжевую).
              0
              Спасибо. Отличная статья. Чуть раньше бы написали — не пришлось бы задавать вопрос по скобкам вокруг max и min:
              stackoverflow.com/questions/19811452/c-calling-function-with-parenthesis-around-its-name
              Думал, это для запрета ADL.
                +1
                Читал сабжевую книгу, в целом, неплохая. Но написана очень сложным для прочтения английским, полным русизмов. Но на безрыбье выбирать не приходится.
                  0
                  По поводу swap: по-моему проще определить move конструктор и оператор присваивания, вместо того, чтобы придумывать свой swap.
                  По поводу:
                  В C++11 было решено, что std::vector и ряд других контейнеров могут использовать move-конструкторы и move-assignment операторы для элементов только если move-конструкторы и move-assignment операторы этих элементов не кидают исключений (ну или если элементы не могут копироваться).

                  А с чем это связано? Не очень понятно, чем в случае кидающего исключения move-конструктора лучше кидающий исключения copy-конструктор.
                    +5
                    В случае конструктора копирования, если N-й элемент при копировании выбросит исключение, то предыдущие N-1 элементов будут существовать одновременно в двух векторах, по этому, в новом векторе можно будет вызвать для них деструкторы, корректно освободить память, а исходный вектор не изменится.
                    Если перемещающий оператор/конструктор сгенерирует исключение, то первые N-1 элемент будут в новом векторе, а N+1 и последующие в старом и безопасно откатиться из этого состояния не получится — исходный вектор окажется в несогласованном состоянии.
                    0
                    Кстати, сейчас издательство проводит новогоднюю распродажу и эта книга стоит всего £3.05 за pdf версию. www.packtpub.com/boost-cplusplus-application-development-cookbook/book
                      0
                      Начнем с достаточно тривиального совета: если вы знаете количество элементов, которые будут вставлены в вектор, то перед вставкой необходимо вызвать reserve


                      В таком виде совет может быть вреден. Например, в STL Microsoft (VC11), если добавлять элементы порциями, то вызов reserve перед каждой порцией значительно снижает производительность по сравнению с дефолтным экспоненциальным расширением. То есть правильнее сказать: "… если знаете максимальное кол-во эл-тов..."

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое