Страницы

Поиск по вопросам

среда, 27 ноября 2019 г.

Как в C/C++ узнать, является ли тип знаковым или беззнаковым?

#c++ #c


Сталкиваясь с типами данных, подобными time_t, size_t и другими, очевидно "численными"
(считая, что указатель это тоже число) типами, иногда становится просто интересно,
а это signed или unsigned тип? 

Как, не роясь во всех include-файлах, где находятся их определения, часто спрятанные
в дебрях условных макроподстановок, выяснить это?

По крайней мере для x86_64 GNU/Linux  и компилятора gcc/g++ можно использовать простой
прием, заключающийся в вычитании единицы из нуля и проверки, меньше ли нуля результат
вычитания. Если меньше, то мы имеем signed тип, а иначе он unsigned.

Такой прием можно оформить парой макросов:

#define SIGNED_TYPE(typename) ({volatile typename v = 0; volatile typename v1 = v
- 1; v > v1;})


определяет знаковость типа  по имени типа, а макрос

#define SIGNED_VAR(var) ({volatile typeof(var) v = 0, v1 = v - 1; v > v1;})


по переменной данного типа.

И если с первым макро все хорошо, то второй работает только при компиляции с флагом
-std=... , определяющим GNU расширения C/C++ (в т.ч. без флага -std=..., т.е. "по умолчанию"
для gcc/g++), а вот для -std=c89, -std=c11 и т.д. не компилируется, поскольку typeof()
это GNU расширение.

Такая модификация макроса для определения "знаковости" переменной

#ifndef __STRICT_ANSI__
// GNU extensions
#define SIGNED_VAR(var) ({volatile typeof(var) v = 0, v1 = v - 1; v > v1;})
#else 
#define SIGNED_VAR(var) ({var = 0; var > var - 1;})
#endif


конечно ужасна, во-первых, макрос меняет значение тестируемой переменной, а во-вторых
(и это главное), неправильно определяет "знаковость" указателя (правильно -- unsigned)
при компиляции в __STRICT_ANSI__ с оптимизацией выше -O1.

Маленькая программа для иллюстрации.

// compile gcc/g++
#include 
#include 
#include 


int
main (int ac, char *av[])
{
  time_t t = 0, t1 = t - 1;
  size_t s = 0, s1 = s - 1;
  char *p = 0, *p1 = p - 1;
  int i = 0, i1 = i - 1,
    *pp = 0, *pp1 = pp - 1;
  double d = 0, d1 = d - 1;

  printf("time_t: %ssigned\n", t1 < t ? "" : "un");
  printf("size_t: %ssigned\n", s1 < s ? "" : "un");
  printf("int:    %ssigned\n", i1 < i ? "" : "un");
  printf("double: %ssigned\n", d1 < d ? "" : "un");
  printf("char *: %ssigned\n", p1 < p ? "" : "un");
  printf("  demo for pointers\n  %p > %p %s\n", 
         pp, pp1, pp > pp1 ? "true" : "false");
  puts("\n  check macro");

#define SIGNED_TYPE(typename) ({volatile typename v = 0; volatile typename v1 = v
- 1; v > v1;})

  printf("SIGNED_TYPE(time_t): %ssigned\n", SIGNED_TYPE(time_t) ? "" : "un");
  printf("SIGNED_TYPE(size_t): %ssigned\n", SIGNED_TYPE(size_t) ? "" : "un");
  printf("SIGNED_TYPE(char *): %ssigned\n", SIGNED_TYPE(char *) ? "" : "un");
  printf("SIGNED_TYPE(int):    %ssigned\n", SIGNED_TYPE(int) ? "" : "un");
  printf("SIGNED_TYPE(double): %ssigned\n", SIGNED_TYPE(double) ? "" : "un");
  printf("SIGNED_TYPE(long long):     %ssigned\n", 
         SIGNED_TYPE(long long) ? "" : "un");
  printf("SIGNED_TYPE(unsigned char): %ssigned\n", 
         SIGNED_TYPE(unsigned char) ? "" : "un");

#ifndef __STRICT_ANSI__
// GNU extensions
#define SIGNED_VAR(var) ({volatile typeof(var) v = 0, v1 = v - 1; v > v1;})
#else 
// UGLY, because change var, WRONG for -std=c++... -std=c... with -Ox
#define SIGNED_VAR(var) ({var = 0; var > var - 1;})
#endif

  printf("SIGNED_VAR(time_t t): %ssigned\n", SIGNED_VAR(t) ? "" : "un");
  printf("SIGNED_VAR(size_t s): %ssigned\n", SIGNED_VAR(s) ? "" : "un");
  printf("SIGNED_VAR(int i):    %ssigned\n", SIGNED_VAR(i) ? "" : "un");
  printf("SIGNED_VAR(double d): %ssigned\n", SIGNED_VAR(d) ? "" : "un");
  printf("SIGNED_VAR(char *p):  %ssigned\n", SIGNED_VAR(p) ? "" : "un");

  return puts("End") == EOF;
}


Какими еще способами можно ответить на данный вопрос и работает ли описанный метод
в системах, отличных от x86_64 GNU/Linux gcc/g++ ?

P.S.
не стоит рассматривать эту задачку, как к имеющую смысл при практическом  программировании,
важную для использования API с такими типами, нет, к ней можно относиться только с
т.з.  удовлетворения естественного любопытства.
    


Ответы

Ответ 1



В С++ для этого есть std::is_signed, std::is_unsigned. В Си можно написать макрос, который будет преобразовывать литерал 0 и выражение -1 к нужному типу: #define IS_SIGNED_TYPE(type) ((type)0 > (type)-1) Для переменных можно было бы использовать var^var чтобы получить 0 нужного типа. Но это работает только для целых чисел. Так же нам надо получить (type)-1 что невозможно если использовать только арифметические операции, например тип var^var - 1 - это int. По этому надо использовать такое расширение компилятора, например __typeof__. #define IS_SIGNED_VAR(var) (IS_SIGNED_TYPE(__typeof__(var)))

Ответ 2



По-моему, в С++ для этого есть такая вещь, как type traits, и конкретно is_signed и is_unsigned... cout << "size_t is" << (is_signed::value ? " " : " un") << "signed\n"; cout << "time_t is" << (is_signed::value ? " " : " un") << "signed\n"; cout << "int is" << (is_signed::value ? " " : " un") << "signed\n"; cout << "long is" << (is_signed::value ? " " : " un") << "signed\n"; Кстати, напомню, что is_signed != !is_unsigned - если для какого-то типа они оба дают false, то, вообще говоря, говорить о знаковости или беззнаковости не приходится. Знаков или беззнаков тип vector? :) И еще - о С++ же и методе, описанном в вопросе. typeof() лучше бы заменить на decltype(), нет? Выражение вида ({...}) и в самом деле непереносимо. Лучше воспользоваться лямбдой: auto signed_var = [](auto x) { volatile decltype(x) z = 0, z1 = z - 1; return z > z1; }; int x; unsigned int z; cout << signed_var(x) << endl; cout << signed_var(z) << endl; Update В С+11 использование type traits требует включения #include Что касается того, к какому стандарту относится эта возможность, то как часть numeric_limits она описана как минимум в С++98 (ISO/IEC 14882:1998). Просто в старых версиях (впрочем, из-за обратной совместимости можно и в новых) надо включать #include и использовать как cout << numeric_limits::is_signed << endl; cout << numeric_limits::is_signed << endl;

Ответ 3



Решение (несколько уродское) уровня c11 для определения знаковости значений на основании _Generic: #include #define IS_SIGNED_TYPE(type) ((type)0 > (type)-1) #define IS_SIGNED_VAR(x) _Generic((x), \ unsigned char: IS_SIGNED_TYPE(unsigned char), \ char: IS_SIGNED_TYPE(char), \ signed char: IS_SIGNED_TYPE(signed char), \ short: IS_SIGNED_TYPE(short), \ unsigned short: IS_SIGNED_TYPE(unsigned short), \ int: IS_SIGNED_TYPE(int), \ unsigned: IS_SIGNED_TYPE(unsigned), \ long: IS_SIGNED_TYPE(long), \ unsigned long: IS_SIGNED_TYPE(unsigned long), \ long long: IS_SIGNED_TYPE(long long), \ unsigned long long: IS_SIGNED_TYPE(unsigned long long), \ float: IS_SIGNED_TYPE(float), \ double: IS_SIGNED_TYPE(double), \ long double: IS_SIGNED_TYPE(long double), \ void*: IS_SIGNED_TYPE(void*), \ default: 0) int main() { unsigned char s; printf("%d\n", IS_SIGNED_VAR(s)); printf("%d\n", IS_SIGNED_VAR(42)); printf("%d\n", IS_SIGNED_VAR(100u)); } Результат clang Результат gcc Замечу, что gcc поддерживает _Generic только с версии 4.9.

Ответ 4



Добавлю просто для коллекции. В С++ можно еще и так проверить: template inline bool _is_signed (T value) { return value = (T)-1, value < 0; } Подобный код и до 11-го стандарта должен отработать.

Ответ 5



И если с первым макро все хорошо, то второй работает только при компиляции с флагом -std=... , определяющим GNU расширения C/C++ (в т.ч. без флага -std=..., т.е. "по умолчанию" для gcc/g++), а вот для -std=c89, -std=c11 и т.д. не компилируется, поскольку typeof() это GNU расширение. Второй, думаю, можно просто привязать к свойству "расширения знака" при сдвиге вправо, что-то вроде такого: #define SIGNED_VAR(var) ((var < 0) || ((~var >> 1) < 0)) Вариант не работает с char и short, т.к. ~var в таком случае преобразуется к типу signed int. для их поддержки придется задействовать typeof: #define SIGNED_VAR(var) ((var < 0) || ((((__typeof__(var))~var) >> 1) < 0)) Пример на ideone

Ответ 6



Применительно к С++ в объектно-ориентированном программировании мы оперируем не типами, а классами. По умолчанию объект какого-либо класса - это ссылка на объект. Например class Time { private: time_t m_time; public: operator int () const; operator unsigned int () const; }; Как следует из приведённого примера, не имеет смысла говорить о том, знакового типа или беззнакового типа является объект класса Time, хотя с точки зрения Си этот объект - просто ячейка в памяти, хранящая значение типа time_t.

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

Отправить комментарий