Кроссплатформенные интегральные типы C++

в 12:36, , рубрики: c++, кроссплатформенное программирование, шаблоны С++, метки: ,

В своей библиотеке стараюсь писать кроссплатформенный код, где это возможно, по стандарту C++, так что для интегральных типов использую только стандартную «десятку» (char, unsigned char, short, unsigned short, int, unsigned int, long, unsigned long, long long, unsigned long long) и никаких виндовых DWORD, __int64 и т.п. Тем не менее, иногда хочется определить тип именно 4 байта, чтобы точно 4, а не «системное слово», «размер адреса» и т.п. Стандарт C++ по этому поводу лишь говорит нараспев: char не может быть больше short, который не может быть больше int, который не может быть больше long, который не может быть больше long long.

Приобщившись к магии обобщённого программирования с помощью шаблонов, решил реализовать кроссплатформенный класс, который генерирует интегральный тип нужного размера во время компиляции. Основные приёмы, который я для этого использую — это рекурентное переопределение типов и частичная специализация шаблонов.

Начнём с формализации определения. Для каждого стандартного типа нужно определить «следующий» тип, необходимый для алгоритма перебора подходящих вариантов. С помощью частичной специализации реализуем шаблон 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> { typedef unsigned long long current_type; };
    template<> struct type_traits <char> { typedef 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> { typedef long long current_type;};

Особенность типов (unsigned) long long в том, что 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 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> { typedef unsigned long long current_type;  typedef unsupported_capacity<capacity> next_type; };
    template<> struct type_traits <char> { typedef 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> { typedef long long 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 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
...

Автор: burov_dmitri

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js