суббота, 9 сентября 2017 г.

[prog.c++] Упарываемся шаблонами: используем их даже для битовых операций

Меня тут давеча на LOR-е типа обвинили в том, что боюсь и шаблонов, и возни с битами и байтами. Юмор этой ситуации заключался в том, что как раз незадолго до этого мы у себя в RESTinio по мере подготовке к релизу очередной публичной версии как раз проводили рефакторинг операций над битами и байтами.

Дело в том, что в коде RESTinio со временем появился ряд операций, в которых требовалось извлечь сколько-то битов из какого-то значения. В принципе, это все элементарные вещи вроде (bs >> 18) & 0x3f. Однако, когда таких элементарных вещей нужно записать несколько штук подряд, да еще в разных местах, да еще с преобразованием результата к разным типам, то лично у меня в голове начинает звучать тревожный звоночек: слишком много хардкодинга и копипасты. А поскольку по поводу копипасты и ее опасности у меня есть собственный пунктик, то в итоге операции с битами и байтами мы упрятали во вспомогательную шаблонную функцию. Там, где у нас было что-то подобное:

result.push_back( alphabet_char( static_cast<char>((bs >> 18) & 0x3f) ) );
result.push_back( alphabet_char( static_cast<char>((bs >> 12) & 0x3f) ) );
result.push_back( alphabet_char( static_cast<char>((bs >> 6) & 0x3f) ) );
result.push_back( alphabet_char( static_cast<char>(bs & 0x3f) ) );

появилось что-то вот такое:

template<unsigned int SHIFT>
char
sixbits_char( uint_type_t bs )
{
   return ::restinio::impl::bitops::n_bits_from< char, SHIFT, 6 >(bs);
}
...
result.push_back( alphabet_char( sixbits_char<18>(bs) ) );
result.push_back( alphabet_char( sixbits_char<12>(bs) ) );
result.push_back( alphabet_char( sixbits_char<6>(bs) ) );
result.push_back( alphabet_char( sixbits_char<0>(bs) ) );

Где ключевую роль играет тривиальная шаблонная функция n_bits_from:

template<
   typename T,
   unsigned SHIFT,
   unsigned BITS_TO_EXTRACT = details::bits_count<T>::count,
   typename F = unsigned int >
T
n_bits_from( F value )
{
   return static_cast<T>(value >> SHIFT) & details::mask<T>(BITS_TO_EXTRACT);
}

Полная реализация n_bits_from в ее текущем виде находится под катом. Но сказать хочется вот что: возможно, это уже оверкилл и злоупотребление шаблонами. Но, как мне кажется, использование n_bits_from в местах, где операции по извлечению битов из байтов производятся в большом количестве, помогает избежать:

  • во-первых, замыливания глаза при повторении однотипных операций. Когда приходится записывать подряд штук 5-6 сдвигов с последующими "логическими И", то очень легко где-то ошибиться и записать не то смещение или не ту битовую маску. Такие ошибки, к сожалению, не так просто заметить и они могут жить в коде очень долго, особенно, если код недостаточно покрыт тестами;
  • во-вторых, неявных приведений типов, которые в C++ могут приводить к неожиданным результатам. Например, можно легко попытаться получить из char-а значение unsigned int, забыв про промежуточный каст в unsigned char. И, если в char-е установлен старший бит, то получить нежданчик. Особенно это круто в ситуации, когда сперва 8 бит извлекаются из int-а в char, а затем этот char используется в качестве индекса в массиве (т.е. может произойти расширение из char в size_t, который беззнаковый).

Так что, может я совсем упорот шаблонами, но как по мне, если сдвиги и "логические И" пошли в коде косяком, то лучше обезопасить себя за счет похожих на n_bits_from вспомогательных функций. Тем более, что оптимизирующие компиляторы для n_bits_from генерируют точно такой же код, как и для вручную записанных битовых операций.

Ну а теперь полная реализация (ее текущий вариант, не факт, что хороший и окончательный):

namespace details {

templatetypename T >
constexpr T mask( unsigned bits_to_extract )
{
   return bits_to_extract <= 1u ? T{1} :
      ((mask<T>(bits_to_extract-1) << 1) | T{1});
}

templatetypename T >
struct bits_count;

template<>
struct bits_count<std::uint8_t> { static constexpr unsigned count = 8u; };

template<>
struct bits_count<std::int8_t> { static constexpr unsigned count = 8u; };

template<>
struct bits_count<char> { static constexpr unsigned count = 8u; };

template<>
struct bits_count<std::uint16_t> { static constexpr unsigned count = 16u; };

template<>
struct bits_count<std::int16_t> { static constexpr unsigned count = 16u; };

template<>
struct bits_count<std::uint32_t> { static constexpr unsigned count = 32u; };

template<>
struct bits_count<std::int32_t> { static constexpr unsigned count = 32u; };

template<>
struct bits_count<std::uint64_t> { static constexpr unsigned count = 64u; };

template<>
struct bits_count<std::int64_t> { static constexpr unsigned count = 64u; };

/* namespace details */

/*!
 * \brief Extract N bits from a bigger integer value.
 *
 * Usage example:
 * \code
 * // Extract 8 bits as unsigned char from bits 24..31 in uint32_t.
 * const std::uint32_t v1 = some_uint_value();
 * const auto u8 = n_bits_from<std::uint8_t, 24>(v1);
 *
 * // Extract 6 bits as char from bits 12..17 in uint32_t.
 * const auto ch = n_bits_from<char, 12, 6>(v1);
 *
 * // Extract 4 bits as unsigned int from bits 32..35 in uint64_t.
 * const std::uint64_t v2 = some_uint64_value();
 * const auto ui = n_bits_from<unsigned int, 32, 4>(v2);
 * \endcode
 * 
 */
template<
   typename T,
   unsigned SHIFT,
   unsigned BITS_TO_EXTRACT = details::bits_count<T>::count,
   typename F = unsigned int >
T
n_bits_from( F value )
{
   return static_cast<T>(value >> SHIFT) & details::mask<T>(BITS_TO_EXTRACT);
}

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