Как печатать типы неизвестного размера, такие как ino_t?

Часто возникают ситуации, когда я хочу напечатать с printf значение целочисленного типа размера, определенного для реализации (например, ino_t или time_t). Прямо сейчас, я использую такой шаблон для этого:

#include <inttypes.h>

ino_t ino; /* variable of unknown size */
printf("%" PRIuMAX, (uintmax_t)ino);

Этот подход работает до сих пор, но он имеет несколько недостатков:

  • Я должен знать, подписан ли тип, который я пытаюсь распечатать, или без знака.
  • Мне нужно использовать тип, который может увеличить мой код.

Есть ли лучшая стратегия?

Ответ 1

#include <inttypes.h>
ino_t ino; /* variable of unknown size */
/* ... */
printf("%" PRIuMAX, (uintmax_t)ino);

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

printf("%ju", (uintmax_t)ino);

Модификатор длины j

Указывает, что следующие d, i, o, u, x, или x спецификатор преобразования применяется к intmax_tили uintmax_t; или что следующая n конверсия specifier применяется к указателю на аргумент intmax_t.

Существуют также модификаторы z и t для size_t и ptrdiff_t (и соответствующие им типы с подписью/без знака) соответственно.

И лично я считаю макросы форматированной строки, определенные в <inttypes.h> уродливыми и трудно запоминающимися, поэтому я предпочитаю "%ju" или "%jd".

Как вы упомянули, полезно знать, подписан ли тип (ino_t в этом случае) или без знака. Если вы этого не знаете, возможно это выяснить:

#include <stdio.h>
#include <stdint.h>
#include <sys/types.h>

#define IS_SIGNED(type) ((type)-1 < (type)0)
#define DECIMAL_FORMAT(type) (IS_SIGNED(type) ? "%jd" : "%ju")
#define CONVERT_TO_MAX(type, value) \
    (IS_SIGNED(type) ? (intmax_t)(value) : (uintmax_t)(value))
#define PRINT_VALUE(type, value) \
    (printf(DECIMAL_FORMAT(type), CONVERT_TO_MAX(type, (value))))

int main(void) {
    ino_t ino = 42;
    PRINT_VALUE(ino_t, ino);
    putchar('\n');
}

хотя это может быть излишним. Если вы уверены, что тип более узкий, чем 64 бита, вы можете преобразовать значение в intmax_t, и значение будет сохранено. Или вы можете использовать uintmax_t и получить четко определенные результаты для всех значений, хотя печать -1 как 18446744073709551615 (2 64 -1) может быть немного запутанной.

Все это работает, только если ваша реализация C поддерживает <stdint.h> и модификатор длины j для printf - то есть, если он поддерживает C99. Не все компиляторы делают это (cough Microsoft cough). Для C90 самые широкие целые типы long и unsigned long, и вы можете конвертировать их в теги и использовать "%ld" и/или "%lu". Теоретически вы можете проверить соответствие C99 с помощью предопределенного макроса __STDC_VERSION__, хотя некоторые компиляторы pre-C99 могут поддерживать типы, более широкие, чем long и unsigned long как расширение.

Ответ 2

"Размер" целочисленного типа здесь не уместен, но его диапазон значений.

По-видимому, вы пытались, однако, можно отличить uintmax_t и intmax_t, чтобы легко решить любую двусмысленность в вызове printf().

Проблема с подписанными или неподписанными типами может быть решена простым способом:

  • Все целые числа без знака работают по модулю "N" для некоторого положительного значения N, в зависимости от типа. Это означает, что каждый результат, включающий только целые числа без знака, дает неотрицательное значение.
  • Чтобы определить, имеет ли переменная x подписанный или неподписанный тип, достаточно проверить, являются ли теги x и -x неотрицательными значениями.

Например:

 if ( (x>=0) && (-x>=0) )
    printf("x has unsigned type");
 else
    printf("x has signed type");

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

(Отредактировано: имя и выражение макроса изменены)

 #include <inttypes.h>
 #include <limits.h>

 #define fits_unsigned_type(N) ( (N >= 0) && (  (-(N) >= 0) || ((N) <= INT_MAX) ) )
 #define smartinteger_printf(N) \
     (fits_unsigned_type(N)? printf("%ju",(uintmax_t)(N)): printf("%jd",(intmax_t) (N)) )
// ....
ino_t x = -3;
printf("The value is: "); 
smartinteger_printf(x);
//.....

Примечание. Подписанный или неподписанный символ переменной не распознается макросом выше, когда значение равно 0. Но в этом случае все работает хорошо, потому что 0 имеет такое же представление битов в подписанных или неподписанных типов.

Первый макрос может быть использован для определения того, имеет ли тип базового типа арифметический объект нестандартный или нет.
Этот результат используется во втором макросе, чтобы выбрать способ печати объекта на экране.

1st REEDITION:

  • Как отметил в своем комментарии Pascal Cuoq, целочисленные рекламные акции должны быть взяты для значений без знака char и short, заданных в диапазоне от int. Это эквивалентно запросу, если значение находится в rango 0 до INT_MAX.

Итак, я изменил имя макроса на fits_signed_type.
Кроме того, я изменил макрос, чтобы принять во внимание положительные значения int.

Макрос fits_unsigned_type может определить, имеет ли объект целых чисел без знака или нет в большинстве случаев.

  • Если значение отрицательное, очевидно, что тип не unsigned.
  • Если значение N положительно, то
    • если -N положительно, то N имеет тип unsigned,
    • Если -N отрицательный, но N находится в диапазоне от 0 до INT_MAX, то тип N может быть signed или unsigned, но он будет соответствовать диапазону положительных значений int, который находится в диапазоне uintmax_t.

2nd REEDITION:

Кажется, что здесь есть подходы к решению одной и той же проблемы. В моем подходе учитывается диапазон значений и целых правил продвижения, чтобы получить правильное напечатанное значение с помощью printf(). С другой стороны, подход Гжегожа Сппетковского определяет подписанный характер типа в прямой форме. Мне нравятся оба.

Ответ 3

Так как вы уже используете заголовок C99, есть возможность использовать спецификатор формата точной ширины в зависимости от sizeof(T) и проверки с подписью/без знака. Однако это необходимо сделать после фазы предварительной обработки (так что грустно ## оператор здесь не может использоваться для создания токена PRI). Вот идея:

#include <inttypes.h>

#define IS_SIGNED(T) (((T)-1) < 0) /* determines if integer type is signed */

...

const char *fs = NULL;
size_t bytes = sizeof(T);

if (IS_SIGNED(T))
    switch (bytes) {
        case 1: fs = PRId8;  break;
        case 2: fs = PRId16; break;
        case 4: fs = PRId32; break;
        case 8: fs = PRId64; break;
    }
else
    switch (bytes) {
        case 1: fs = PRIu8;  break;
        case 2: fs = PRIu16; break;
        case 4: fs = PRIu32; break;
        case 8: fs = PRIu64; break;
    }

С помощью этого метода приведение не требуется больше, однако форматировать строку необходимо выполнить вручную, прежде чем передать ее в printf (т.е. автоматическое конкатенация строк). Вот несколько примеров:

#include <stdio.h>
#include <inttypes.h>

#define IS_SIGNED(T) (((T)-1) < 0)

/* using GCC extension: Statement Expr */
#define FMT_CREATE(T) ({                      \
    const char *fs = NULL;                    \
    size_t bytes = sizeof(ino_t);             \
                                              \
    if (IS_SIGNED(T))                         \
        switch (bytes) {                      \
            case 1: fs = "%" PRId8;  break;   \
            case 2: fs = "%" PRId16; break;   \
            case 4: fs = "%" PRId32; break;   \
            case 8: fs = "%" PRId64; break;   \
        }                                     \
    else                                      \
        switch (bytes) {                      \
            case 1: fs = "%" PRIu8;  break;   \
            case 2: fs = "%" PRIu16; break;   \
            case 4: fs = "%" PRIu32; break;   \
            case 8: fs = "%" PRIu64; break;   \
        }                                     \
    fs;                                       \
})

int main(void) {
    ino_t ino = 32;

    printf(FMT_CREATE(ino_t), ino); putchar('\n');

    return 0;
}

Обратите внимание, что для этого требуется небольшая обманка Statement Expr, но может быть и другой способ (это "цена" должна быть общей) также.

EDIT:

Вот вторая версия, которая не требует специального расширения компилятора (не беспокойтесь, я тоже не могу ее прочитать), используя макрос функции:

#include <stdio.h>
#include <inttypes.h>

#define IS_SIGNED(T) (((T)-1) < 0)
#define S(T) (sizeof(T))

#define FMT_CREATE(T)   \
    (IS_SIGNED(T)        \
        ? (S(T)==1?"%"PRId8:S(T)==2?"%"PRId16:S(T)==4?"%"PRId32:"%"PRId64) \
        : (S(T)==1?"%"PRIu8:S(T)==2?"%"PRIu16:S(T)==4?"%"PRIu32:"%"PRIu64))

int main(void)
{
    ino_t ino = 32;

    printf(FMT_CREATE(ino_t), ino);
    putchar('\n');

    return 0;
}

Обратите внимание, что условный оператор оставил assiociativity (таким образом, он вычисляется слева направо, как предполагалось).

Ответ 4

Используя общие макросы типа C11, можно построить строку формата во время компиляции, например:

#include <inttypes.h>
#include <limits.h>
#include <stdint.h>
#include <stdio.h>

#define PRI3(B,X,A) _Generic((X), \
                             unsigned char: B"%hhu"A, \
                             unsigned short: B"%hu"A, \
                             unsigned int: B"%u"A, \
                             unsigned long: B"%lu"A, \
                             unsigned long long: B"%llu"A, \
                             signed char: B"%hhd"A, \
                             short: B"%hd"A, \
                             int: B"%d"A, \
                             long: B"%ld"A, \
                             long long: B"%lld"A)
#define PRI(X) PRI3("",(X),"")
#define PRIFMT(B,X,A) PRI3(B,(X),A),(X)

int main () {
    signed char sc = SCHAR_MIN;
    unsigned char uc = UCHAR_MAX;
    short ss = SHRT_MIN;
    unsigned short us = USHRT_MAX;
    int si = INT_MIN;
    unsigned ui = UINT_MAX;
    long sl = LONG_MIN;
    unsigned long ul = ULONG_MAX;
    long long sll = LLONG_MIN;
    unsigned long long ull = ULLONG_MAX;
    size_t z = SIZE_MAX;
    intmax_t sj = INTMAX_MIN;
    uintmax_t uj = UINTMAX_MAX;

    (void) printf(PRIFMT("signed char       : ", sc, "\n"));
    (void) printf(PRIFMT("unsigned char     : ", uc, "\n"));
    (void) printf(PRIFMT("short             : ", ss, "\n"));
    (void) printf(PRIFMT("unsigned short    : ", us, "\n"));
    (void) printf(PRIFMT("int               : ", si, "\n"));
    (void) printf(PRIFMT("unsigned int      : ", ui, "\n"));
    (void) printf(PRIFMT("long              : ", sl, "\n"));
    (void) printf(PRIFMT("unsigned long     : ", ul, "\n"));
    (void) printf(PRIFMT("long long         : ", sll, "\n"));
    (void) printf(PRIFMT("unsigned long long: ", ull, "\n"));
    (void) printf(PRIFMT("size_t            : ", z, "\n"));
    (void) printf(PRIFMT("intmax_t          : ", sj, "\n"));
    (void) printf(PRIFMT("uintmax_t         : ", uj, "\n"));
}

Однако существует потенциальная проблема: если существуют типы, отличные от перечисленных (т.е. кроме signed и unsigned версий char, short, int, long и long long), это не работает для этих типов. Также невозможно добавить типы, такие как size_t и intmax_t, в общий шаблон типа "на всякий случай", потому что это вызовет ошибку, если они являются typedef d из одного из уже перечисленных типов. Я оставил случай default неуказанным, чтобы макрос генерировал ошибку времени компиляции, когда не найдено подходящего типа.

Однако, как видно из примера программы, size_t и intmax_t отлично работают на платформах, где они совпадают с одним из перечисленных типов (например, такими же, как long). Аналогично, нет проблем, если, например, long и long long, или long и int, являются одним и тем же типом. Но более безопасной версией может быть просто приведение к intmax_t или uintmax_t в соответствии с подписью (как видно из других ответов) и создание типового макроса с только этими параметрами...

Косметическая проблема заключается в том, что макрос типового типа расширяется с круглыми скобками вокруг строкового литерала, предотвращая конкатенацию со смежными строковыми литералами обычным способом. Это предотвращает такие вещи, как:

(void) printf("var = " PRI(var) "\n", var); // does not work!

Следовательно, макрос PRIFMT с префиксом и суффиксом включен для общего случая печати одной переменной:

(void) printf(PRIFMT("var = ", var, "\n"));

(Обратите внимание, что было бы просто развернуть этот макрос с нецелыми типами, поддерживаемыми printf, например, double, char *...)