суббота, 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 генерируют точно такой же код, как и для вручную записанных битовых операций.

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

пятница, 8 сентября 2017 г.

[prog.c++] json_dto-0.2

Мы сегодня обновили свою небольшую библиотеку json_dto, которая служит хоть и тонкой, но очень полезной оберткой над такой замечательной штукой, как rapidjson. Мы сделали json_dto где-то года полтора назад, взяв идеи из Boost.Serialization, для уменьшения объема писанины при работе с JSON в C++. Например, json_dto позволяет писать вот так:

class User {
public :
   /* ... */

   template<typename JSON_IO>
   void json_io(JSON_IO & io) {
      io & json_dto::mandatory("id", _id)
         & json_dto::mandatory("name", _name)
         & json_dto::mandatory("birthday", _birthday)
         & json_dto::mandatory("phone", _phone);
   }
};

вместо того, чтобы писать вот такие простыни (взято отсюда как пример несложного кода):

вторник, 5 сентября 2017 г.

[prog.thoughts] Сложные инструменты уже не нужны в современных условиях?

Размышляя время от времени над феноменом языка Go, над тем, что пишут на Хабре или обсуждают на профильных ресурсах (типа LOR-а или RSDN-а), закрадывается мысль, что в современных условиях сложные инструменты мало кому нужны. И я не могу понять, это объективная реальность такова или же это я вошел в пору конфликта "отцов и детей", но уже со стороны "отцов".

Но вот взять тот же C++, который сейчас повсеместно ругают за сложность. Забывая при этом, что сложность эта возникла не просто так, а как адаптация языка к тем прикладным нишам, в которых он активно используется. Сложная система шаблонов в C++ появилась не просто так же. Специализация шаблонов, к примеру, является следствием того, что обобщенные структуры и/или алгоритмы бывает нужно адаптировать к конкретным условиям. Ну это же объективная реальность такая: ты либо борешься со сложностью свой прикладной задачи посредством мощного инструмента, либо же тупо тратишь больше времени и усилий, обходясь намного более примитивными средствами. Скажем, там, где можно сделать один шаблон и применить его для 5 разнотипных наборов данных, можно же обойтись и без шаблона, посредством копипасты.

Мне часто вспоминается первое знакомство с C++ в далеком 1991-ом году. Язык был гораздо сложнее Паскаля и Бейсика. Но зато он позволял сделать свои классы Set и Matrix, которые бы ничем не отличались бы от встроенных в язык типов. А уж когда в C++ завезли шаблоны, то тут вообще такие бескрайние просторы открылись, что просто дух захватывало. Можно было сделать Matrix<T>, где T мог быть, скажем, Complex<U>, да и сам U мог быть не просто float-ом или double, а каким-то собственным FixedSizeFloat...

Конечно, C++ здесь не очень показательный пример, т.к. за его мощность нужно было платить унаследованными от C граблями. Но можно посмотреть и на другие знаковые языки 1980-х и 1990-х годов. Ada, например. Или Eiffel. Да взять ту же Java, которая вышла в свет в 1995-ом как очень примитивный язык. Который был вынужден со временем усложниться и заиметь таки генерики. С C# затем это так же произошло, но гораздо быстрее.

Т.е. в 1980-х и 1990-х, да даже в начале 2000-х, растущая сложность инструментария воспринималась как само собой разумеющееся. Думаю, что это происходило потому, что программистов было мало, сложность программ росла очень быстро, потребность в софте росла еще быстрее. Получалось, что когда программистов мало, а задача сложная, то решать ее методом грубой силы не получится, просто нет ресурсов. Значит решать ее можно было посредством более мощных, а значит и более сложных, инструментов.

Но потом что-то изменилось. Возможно, двумя последними языками из знакомых мне, которые пошли по пути создания сложного, но мощного инструмента, были D и Scala. А вот то, что стало появляться затем, образует уже иную тенденцию. Go -- это вообще какой-то крайний случай. А вот языки, вроде, Ceylon, Kotlin и даже Rust, как мне кажется, идут по пути снижения сложности, но при этом предоставления пользователям достаточной мощности. Хотя "достаточной" -- это относительное понятие. Например, нет в Rust-е нормального ООП и кому-то Rust может казаться достаточно мощным, а кому-то -- нет.

Думается, что все это таки объективно. Подавляющему большинству разработчиков сейчас не нужны сложные инструменты. Ибо изменился "ландшафт" программирования. Сложных задач в процентном отношении становится меньше. Больше становится рутины, в которой основная сложность не в самом программировании, а в организации процесса разработки: от общения с заказчиком и формализации требований до интеграционного тестирования и запуска в эксплуатацию. Программист -- это винтик, который должен быть быстрообучаемым и легкозаменяемым. Чего сложно достигнуть, если программист будет использовать C++ или Scala, а не Go или Kotlin.