Приобщившись к магии обобщённого программирования с помощью шаблонов, решил реализовать кроссплатформенный класс, который генерирует интегральный тип нужного размера во время компиляции. Основные приёмы, который я для этого использую — это рекурентное переопределение типов и частичная специализация шаблонов.
Начнём с формализации определения. Для каждого стандартного типа нужно определить «следующий» тип, необходимый для алгоритма перебора подходящих вариантов. С помощью частичной специализации реализуем шаблон type_traits:
template<typename type> struct type_traits;
template<> struct type_traits <unsigned char> { typedef unsigned char current_type; typedef unsigned short next_type; };
template<> struct type_traits <unsigned short> { typedef unsigned short current_type; typedef unsigned int next_type; };
template<> struct type_traits <unsigned int> { typedef unsigned int current_type; typedef unsigned long next_type; };
template<> struct type_traits <unsigned long> { typedef unsigned long current_type; typedef unsigned long long next_type; };
template<> struct type_traits <unsigned long long int> { typedef unsigned long long int current_type; };
template<> struct type_traits <signed char> { typedef signed char current_type; typedef short next_type; };
template<> struct type_traits <short> { typedef short current_type; typedef int next_type; };
template<> struct type_traits <int> { typedef int current_type; typedef long next_type; };
template<> struct type_traits <long> { typedef long current_type; typedef long long next_type; };
template<> struct type_traits <long long int> { typedef long long int current_type;};
Особенность типов (unsigned) long long int в том, что next_type для них не определён, так как больше них гарантированно ничего нет.
Далее следует определить основной шаблон алгоритма выбора, который содержит два параметра: type — это стандартный числовой тип и bool'евую переменная, которая равна true, если данный тип подходит по размеру или false если не подходит. В реализации по умолчанию берём у type «текущий тип» — type_traits::current_type, а в случае если тип не подходит, берём «следующий» — type_traits::next_type:
// Алгоритм выбора типа
template<typename type, bool>
struct type_choice
{
typedef typename type_traits<type>::current_type std_type;
};
template<typename type>
struct type_choice<type, false>
{
typedef typename type_traits<type>::next_type next_type;
typedef typename type_choice<next_type, sizeof(next_type) == capacity>::std_type std_type;
};
Третий служебный шаблон предназначен для выбора начального варианта, которых у нас два, в зависимости от того, хотим ли мы использовать знаковый или беззнаковый тип — char или unsigned char:
// Базовый тип для начала подбора
template <bool is_signed> struct base_type_selector { typedef signed char base_type; };
template <> struct base_type_selector<false> { typedef unsigned char base_type; };
Наконец, нужно определить главный класс, который и будет содержать нужный тип. Я назвал этот класс fixed_int, он имеет два шаблонных параметра: первый параметр имеет тип size_t и обозначает желаемую ёмкость в байтах, второй параметр булевый и отвечает за знак типа. Сам же класс из открытых сущностей содержит лишь один волшебный typedef:
typedef typename type_choice< typename base_type_selector<is_signed>::base_type, sizeof(base_type_selector<is_signed>::base_type) == capacity >::std_type type;
Компоновать главный и служебные классы можно по разному. Мудрый компилятор MVS компилит локальные шаблонные классы без запинок:
template <size_t capacity, bool is_signed>
class fixed_int
{
// Описание ошибки компиляции в случае использования не поддерживаемой размерности
template <int x> struct unsupported_capacity { int i[1/(x-x)]; };
template <> struct unsupported_capacity<1> {};
template <> struct unsupported_capacity<2> {};
template <> struct unsupported_capacity<4> {};
template <> struct unsupported_capacity<8> {};
// Свойства базовых типов, необходимые для перебора
template<typename type> struct type_traits;
template<> struct type_traits <unsigned char> { typedef unsigned char current_type; typedef unsigned short next_type; };
template<> struct type_traits <unsigned short> { typedef unsigned short current_type; typedef unsigned int next_type; };
template<> struct type_traits <unsigned int> { typedef unsigned int current_type; typedef unsigned long next_type; };
template<> struct type_traits <unsigned long> { typedef unsigned long current_type; typedef unsigned long long next_type; };
template<> struct type_traits <unsigned long long int> { typedef unsigned long long int current_type; typedef unsupported_capacity<capacity> next_type; };
template<> struct type_traits <signed char> { typedef signed char current_type; typedef short next_type; };
template<> struct type_traits <short> { typedef short current_type; typedef int next_type; };
template<> struct type_traits <int> { typedef int current_type; typedef long next_type; };
template<> struct type_traits <long> { typedef long current_type; typedef long long next_type; };
template<> struct type_traits <long long int> { typedef long long int current_type; typedef unsupported_capacity<capacity> next_type;};
// Алгоритм выбора типа
template<typename type, bool>
struct type_choice
{
typedef typename type_traits<type>::current_type std_type;
};
template<typename type>
struct type_choice<type, false>
{
typedef typename type_traits<type>::next_type next_type;
typedef typename type_choice<next_type, sizeof(next_type) == capacity>::std_type std_type;
};
// Базовый тип для начала подбора
template <bool is_signed> struct base_type_selector { typedef signed char base_type; };
template <> struct base_type_selector<false> { typedef unsigned char base_type; };
public:
typedef typename type_choice< typename base_type_selector<is_signed>::base_type, sizeof(base_type_selector<is_signed>::base_type) == capacity >::std_type type;
};
Менее умные компиляторы могут такой конструкции не понять, например Qt жалуется на частичную специализацию шаблонного класса внутри другого шаблонного класса. Для таких случаев служебные внутренние шаблоны можно вынести отдельно в namespace __private, чтобы не замусоривать общее пространство имён, такой способ в подобных случаях использует Александреску в своей библиотеке Loki (например, для списков типов).
Осталось добавить удобные имена для всех типов, например так:
typedef fixed_int<1, false>::type uint8;
typedef fixed_int<2, false>::type uint16;
typedef fixed_int<4, false>::type uint32;
typedef fixed_int<8, false>::type uint64;
typedef fixed_int<1, true>::type int8;
typedef fixed_int<2, true>::type int16;
typedef fixed_int<4, true>::type int32;
typedef fixed_int<8, true>::type int64;
… и проверить что-же из всего этого получилось (запущено под MVS2015/intel 0x86):
...
int32 x1;
uint64 x2;
fixed_int<2, true>::type x3;
std::wcout<<typeid(x1).name()<<std::endl;
std::wcout<<typeid(x2).name()<<std::endl;
std::wcout<<typeid(x3).name()<<std::endl;
...
Результат:
int
unsigned __int64
short
Итого, мы получили кроссплатформенные фиксированные типы, не использующие никакой сторонней информации для своего определения. В качестве платы идут дополнительные вычисления на этапе компиляции. Неправильные параметры шаблона или невозможность поддержать данную размерность на какой-нибудь платформе приведут к ошибке компиляции, что также является плюсом.
P.S.: Так как описание ошибок инстанцирования шаблонов страдают некоторой сложностью, я использовал небесспорный приём: определение шаблонного вспомогательного класса, у которого компилируются только частичные специализации:
// Описание ошибки компиляции в случае использования не поддерживаемой размерности
template <int x> struct unsupported_capacity { int i[1/(x-x)]; };
template <> struct unsupported_capacity<1> {};
template <> struct unsupported_capacity<2> {};
template <> struct unsupported_capacity<4> {};
template <> struct unsupported_capacity<8> {};
Небесспорный главным образом потому, что описание ошибок в стандарте не определены, и потом польза такого класса не гарантированна. Компилятора Microsoft при попытке инстанцировать вот такой тип fixed_int<3, true>::type выдаёт ошибку:
exp4.cpp(127): error C2057: expected constant expression
exp4.cpp(156) : see reference to class template instantiation 'fixed_int<3,true>::unsupported_capacity<3>' being compiled
...
P.S.: Заменил типы char на signed char и (unsigned) long long на (unsigned) long long int
Комментарии (73)
kosmos89
30.03.2016 15:44+7А чем cstdint не угодил?
burov_dmitri
30.03.2016 16:23-4Да почему же, он всем хорош. Наверное это и вправду велосипед, но я привык уже его использовать
khim
30.03.2016 17:33+5Хотите — используйте. Но хотя бы замените в нём char на signed char, если хотите переносимости!
burov_dmitri
30.03.2016 19:36Вы правы, если уж я сослался на ISO, то нужно и long long заменить на long long int, хотя честно я не работал в системах где нет таких синонимов.
mayorovp
30.03.2016 20:11+3Дело не в синонимах. Дело в том, что тип данных char может быть как знаковым, так и нет, в зависимости от настроек компилятора.
khim
30.03.2016 20:16Именно. А "long long" — это как раз стандарт. "Неявный int" можно опускать...
burov_dmitri
31.03.2016 01:35… и я тоже так считал, однако в 3.9.1. пунктах 2 и 3 указан именно long long int (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf).
khim
31.03.2016 02:58При чём тут 3.9.1? Там типы перечислены. Как их можно в программе называть — в 7.1.6.2 написано.
Hokum
30.03.2016 15:48+1Интересное решение. А чем Вас не устраивают целочисленные типы из стандарта C++11?
burov_dmitri
30.03.2016 19:47-1Типы из cstdint меня устраивают полностью, я написал класс вычисляемого размера в 2011-м и тогда даже не знал о cstdint.
StrangerInRed
30.03.2016 15:52-23«Вот тебе шаблоны — начинай велосипедить»
oh my fucking god.
Шаблонная магия С++ разрабов, как они есть.
P.S. Вот он уровень инвайтов на хабре.rotor
30.03.2016 18:45+4Вам не надоело тролить в каждом посте по C++? Или этоту вас весеннее?
StrangerInRed
30.03.2016 19:55-12Собираю лулзов с бугуртящих С++-девов.
Жаль что stdint.h запретили. Или как там у вас, cstdint.h
А чо тролить? Это ж откровенный велосипед, велосипеднее уже некуда карл.
Чуваку надо было написать свой язык со своими типами.
RPG18
30.03.2016 16:39+1На самом деле, кто хотел на VS иметь stdint.h, тот имел благодаря проекту msinttypes.
VioletGiraffe
30.03.2016 16:43Вообще-то в VS и так есть этот хедер, сколько себя помню. С VS2012 точно.
comargo
30.03.2016 20:59stdint.h появился в VisualStudio начиная с десятой версии (которая VS 2010). В девятой версии (VS2008) ее нет.
khim
30.03.2016 17:29+2А насчёт "десятки стандартных типов" — это такая шутка? Вообще-то стандартных типов не 10, а 11. Про "signed char" вы забыли. Тот факт что по-умолчанию char может быть не только теоретически, но и практически любым выпило много крови тем, кто переносит код с x86 (где на большинстве компиляторов он знаковый) на ARM (где по умолчанию он беззнаковый).
burov_dmitri
30.03.2016 21:04Я уже отписался выше, это не шутка, это ошибка. Конечно signed char как и long long int. Конечно о 10-ке (как и о 11-и) говорить не корректно, лучше просто сослаться на параграф 3.9.1., пункты 2 и 3 http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf
khim
30.03.2016 17:43Только сейчас прочитав статью заметил "небесспорный приём". Во-первый — он таки "бесспорный" (см. SFINAE). Во-вторых — ненужный.
В большинстве случаев будет достаточным сделать так:
template <int x> struct unsupported_capacity; template <> struct unsupported_capacity<1> {}; template <> struct unsupported_capacity<2> {}; template <> struct unsupported_capacity<4> {}; template <> struct unsupported_capacity<8> {};
Это, правда, даст возможность описывать указатели на, скажем, "struct unsupported_capacity<3>" — но практически этого достаточно: где-то же в программе вы этот указатель захотите-таки разименовать? Вот там и получите ошибку...Mingun
30.03.2016 18:34Мне кажется, под спорностью автор имел вообще использование такого вспомогательного класса, а не то, что он имеет тело с массивом неизвестного размера.
Кстати, размер проще было просто делать отрицательным:
int i[8-x];
burov_dmitri
30.03.2016 21:06Спорность в том, что результат не гарантирован, а подыгран. Я не нашёл, чтобы стандарт предписывал в какой форме IDE должен сообщать об ошибках инстанцирования
lorc
30.03.2016 19:47+3Хочу немного позанудствовать. Вам никто не обещал что char будет иметь размер в один октет. Иными словами, тот факт что sizeof(char) == 1 не значит что char занимает ровно 8 бит. Он может быть размером 32 бита.
Вряд ли ваш код попадет на платформу где такое имеет бысто быть. Но если вдруг попадет — он там попросту сломается.
Поэтому, он не совсем кроссплатформенный.
Стандарт, кстати, гарантирует только сущесвование типов а-ля int_leastХХ_t и говорит что могут существовать системы где intXX_t просто не определен.StrangerInRed
30.03.2016 20:00-1POSIX обещал. Там где есть поддержка POSIX sizeof(char) == 1
MikalaiR
30.03.2016 21:00+11 байт != 8 бит, как ни странно. 1 байт — минимальная адресуемая единица памяти.
StrangerInRed
30.03.2016 21:15Парень не оперирует словом байт. Он оперирует словом ОКТЕТ. Будьте добры загуглите и поймите что октет это всегда строго 8 бит.
MikalaiR
30.03.2016 21:22+1"Парень" прав. 1 байт не всегда == 1 октет. На x86 они равны, но есть архитектуры, где 1 байт = 4 октета.
StrangerInRed
30.03.2016 21:28+1>sizeof(char) == 1
имеется ввиду именно один октет, в стандарте так и написано
An individually addressable unit of data storage that is exactly an octet, used to store a character or a portion of a character; see also Character. A byte is composed of a contiguous sequence of 8 bits. The least significant bit is called the «low-order» bit; the most significant is called the «high-order» bit.MikalaiR
30.03.2016 21:35+1Тогда извиняюсь, был неправ. Но оператор sizeof всё-таки возвращает размер в байтах, а не в октетах.
khim
30.03.2016 21:49+2По моему вы спорите "мимо" друг друга. Есть стандарт ANSI/ISO C (на самом деле теоретически это разные стандарты). Есть стандарт POSIX (который, кстати, Windows и не поддерживает сегодня — но поддерживала много лет назад).
В ANSI/ISO C размер байта не фиксирован. Может быть 8 бит, может быть 9, а может и все 36. Как карта ляжет. В POSIX — размер байта фиксирован. 8 бит без вариантов. Хотите ли вы поддерживать не-POSIX — системы (+Windows: из-за "наследственности там много чего реализовано "как в POSIX"), или весь спектр ANSI/ISO C систем? Это уж вам решать...StrangerInRed
31.03.2016 08:33-4Я говорил конкретно про POSIX. И не надо тут разводить про поддержку не-POSIX. Была четкая конкретика.
khim
31.03.2016 11:24+3Не очень понятно с какого перепугу вы завели разговор про POSIX при том что он, в общем-то, начался с не-POSIX-совместимой платформы!
lorc
31.03.2016 13:45sizeof(char)==1 везде, на любой системе где есть компилятор C или C++.
Другое дело, что только POSIX гарантирует что длина байта равно 8 бит. ANSI C этого не обещает.
izvolov
30.03.2016 20:15-1Гениально. Автор, ты изобрёл линейный поиск.
using integral_types = type_list<...>; using uint8_t = find_if_t<integral_types, is_unsigned && has_8_bits>; ... // Образно выражаясь.
Eric50
30.03.2016 21:07+1Потерян ещё один тип: «signed char».
Действительно «signed int» идентичен типу «int».
А вот: «unsigned char», «signed char» и «char», это три разных типа.
menzoda
30.03.2016 21:07+2Ну и кроме того не «интегральные», а «целочисленные». Интегральный это от слова integral, и связано это понятие с такой математической закорючкой. А целочисленный — это integer.
develop7
30.03.2016 21:11-1Prelude> :i Integral class (Real a, Enum a) => Integral a where quot :: a -> a -> a rem :: a -> a -> a div :: a -> a -> a mod :: a -> a -> a quotRem :: a -> a -> (a, a) divMod :: a -> a -> (a, a) toInteger :: a -> Integer -- Defined in ‘GHC.Real’ instance Integral Word -- Defined in ‘GHC.Real’ instance Integral Integer -- Defined in ‘GHC.Real’ instance Integral Int -- Defined in ‘GHC.Real’
Eric50
30.03.2016 21:23-7Логично называть целые числа со знаком "integer". И даже приемлимо всякое "int32" или "i32".
А вот например названия "uint32" или даже "u32", уже нелогичны. Особенно если незнать о происхождении.
Зато подходит иное название "натуральное число". Как вам: "natural32", "natural64"?
Я для своих домашних проектиков давно создал хидер с псевдонимами:
typedef signed int32 integer32;
typedef unsigned int32 natural32;
Мне так больше нравится. Более логичные названия.khim
30.03.2016 21:57+6Срочно читать Фейнмана. Про "улучшенную тригонометрию". uint32_t может выглядеть безумием — но это стандарт. Любой, кто использует C/C++ должен про него знать. Хотя на практике, конечно, есть люди знающие C/C++ на уровне Эллочки-людоедки, но им ваши обозначения тоже не помогут.
В программировании и без вас достаточно сложностей и непонятностей — незачем изобретать новые. С одной стороны ваши "домашние проектики" — они лично ваши и вы можете делать что хотите. С другой — а вдруг по работе пригодится? Всё переписывать только из-за того, что вам uint32_t не нравится?
MacIn
30.03.2016 21:59+1Более логичные с точки зрения кого? Signed/unsigned — стандарт. В процессоре (если говорить о x86) отдельно разведены команды signed и unsigned арифметики.
JIghtuse
30.03.2016 23:01Дополню соседние комментарии — даже если предположить, что использование такого псевдонима оправдано, его имя не соответствует действительности.
Натуральные числа — это ряд чисел 1, 2, 3, …, представляющих собой число предметов или более строго — мощности (количества элементов) непустых конечных множеств.
Первая ссылка в гугле
unsigned же может иметь нулевое значение, ломая всю "абстракцию".
rotor
30.03.2016 23:08Кстати, в натуральном ряде нет нуля, если уж вы о чистоте наименований.
Таким образом ваш тип — это целые неотрицательные числа.
Поэтому стандартное обозначение лучше отражает истиное положение вещей чем ваше.Eric50
30.03.2016 23:14-1Спорно, является ли ноль натуральным числом. Некоторые допускают.
Но я вас понял. Всем спасибо за вразумление. Исправлюсь!rotor
30.03.2016 23:52Тут не очем спорить. Это математическое понятие, которое строго определено и не допускает никаких неоднозначностей.
khim
31.03.2016 03:05+1Если бы всё было так однозначно то не было бы всех этих ?0, ?0, ?*, ?+, ?1, ?>0...
rotor
31.03.2016 09:26-1Да, похоже, я был не прав. У меня математическое образование и мы всегда оперировали примерно таким
определениемотсюда:
mayorovp
30.03.2016 23:44+3В английском языке, когда речь идет о типах данных, "integral" — это прилагательное, а "integer" — это существительное.
"integral" переводится как "целочисленный", а "integer" — как "целое число".
Значение "математическая закорючка" у слова "integral" тоже есть, но в этом значении это слово является уже существительным, а не прилагательным, поэтому никто не путается (кроме программистов по всему миру).MacIn
31.03.2016 00:37+1Так то в английском. Здесь же проблема неверного перевода. Как, например, с "органической едой". Или "софистицировать". Или "экспертиза". Толпы их.
mayorovp
31.03.2016 09:50+1Да, но перевод из того комментария, на который я отвечал, столь же неверный. Особенно в части
А целочисленный — это integer.
McAaron
31.03.2016 11:55+2Даже на одной аппаратной платформе, например x86_64 (ia32e), разные ОС по разному интерпретируют резиновые целочисленные типы, введенные в С/C++ в качестве базовых. Резиновые они потому, что стандарт не определяет их длины, он только вводит отношения
sizeof(short int) \le sizeof(int) \le sizeof(long int) \le sizeof(long long int)
Это значит, что не исключается случай, когда все эти типы будут иметь одинаковую длину.
Разумеется, использовать эти типы можно лишь в том случае, если программа ничего не выводит и не выводит, потому что даже одна и та же программа, скомпилированная на разных платформах и/или разными компиляторами не сможет ввести данных, которые она вывела. Чтобы преодолеть это были введены жесткие типы, которые определены в stdint.h (). Эти типы локально настраиваются на соответсвующие резиновые в каждом конкретном случае установки компилятора на конкретную платформу, если таковые там присутствуют.
Как только возникает необходимость в бинарном выводе/вводе, эти типы должны использоваться безусловно. Они обладают абсолютной переносимостью, поскольку описываются в обязательной части стандарта.
Кстати, аналогичная ситуация может случиться при выводе/вводе символов — байт, вопреки расхожему мнению, это не 8 бит, а минимальная порция данных в адресуемой оперативной памяти. Так что запросто может случиться, что char может занимать, например, 12 бит.
В этой связи гораздо более полезно читать стандарт, чем толстые книги, переиздаваемые чуть ли не каждый год. Кстати, стандарт нужно читать не весь, а только до приложений, поскольку в приложения попадают ангажированные тексты спонсоров, пытающиеся возглавить то, что не удается разрушить.
roman_kashitsyn
31.03.2016 14:49+2Если не дают использовать новый стандарт, всегда можно взять
<boost/cstdint.hpp>
.
vladon
01.04.2016 17:02Вы большой молодец и у вас много времени. Но зачем это нужно было-то? На дворе 2016 год.
torkve
torkve
На всякий случай: это есть в стандарте
khim
Ну тут как бы сама преамбула немного намекает на тему, что это типичный случай когда "рад бы в рай, да грехи не пускают". Windows — сильно особая платформа. Там больше 10 лет ушло на то, чтобы этот простейший заголовочный файл создать. Но да, не то в 2010й, не то в 2012й версии он наконец-то появился. Уря! Костыли можно потихоньку выкидывать...
torkve
<сарказм>А что же делать с VC 6, она же наверное эти вообще шаблоны не скомпилирует</сарказм>
khim
<прикинувшись ветошью>Почему это не скомпилирует? Скомпилирует, скорее всего. Сейчас под рукой VC 6 нету, но я не вижу тут ничего, что могло бы помешать этому на VC 6 собраться. Там проблемы с частичной специализацией были, но тут её вроде как нету...</прикинувшись ветошью>
На самом деле я сталкивался с проектами, собиравшимися VC 6 ещё не так давно. Вы думаете статьи подобные этой на ровном месте возникают?
torkve
Я думаю и даже знаю, что до сих пор есть современные проекты, которые пишут на ASP (не ASP.Net) и PHP4. Но надо ж как-то из анабиоза выходить.
burov_dmitri
Может и не скомпилить. Qt компилит, только если вынести внутренние шаблоны из класса. Сам класс я написал в 2011-м году и честно говоря тогда даже не знал о stdint.h
Antervis
по факту, stdint.h появился в том же самом 11-м стандарте, в котором появились type_traits (всякие std::is_signed и подобное), которые ты использовал в своем хедере
Ogra
stdint.h появился в стандарте С99. Это cstdint появился в 11-м стандарте =)
FoxCanFly
?
Gorthauer87
А разве в сети не найти файл stdint.h? когда у иная возникла проблема его отсутствия, я его выбрал из mingw и всё.
Хотя в качестве концепта такая магия может и интересна где нибудь на bare metal железе. Но даже для них можно взять newlib.
MacIn
Это инженерный подход, так не интересно! sarcasm mode off
Gorthauer87
А разве в сети не найти файл stdint.h? когда у иная возникла проблема его отсутствия, я его выбрал из mingw и всё.
Хотя в качестве концепта такая магия может и интересна где нибудь на bare metal железе. Но даже для них можно взять newlib.