4
\$\begingroup\$

Motivation:

Type traits are useful in defining robust function-like macros. Code below has:

  • IS_COMPATIBLE(EXPR, T)
  • IS_NULLPTR(T)
  • IS_FILE_PTR(T)
  • IS_ARRAY(T)
  • IS_POINTER(T)
  • IS_CHAR_SIGNED
  • IS_SIGNED(T)
  • IS_UNSIGNED(T)
  • IS_FLOATING_POINT(T)
  • IS_ARITHMETIC(T)
  • IS_C_STR(T)
  • IS_COMPATIBLE_WITH_ARRAY_OF_LENGTH_N(T, N)
  • IS_ULA_OR_VLA(T)
  • IS_FUNCTION(T)

All of them are documented in detail, so I would not repeat their descriptions here.

Sample use-case for IS_ARRAY():

/**
 * Like C11's _Static_assert() except that it can be used in an expression.
 *
 * EXPR - The expression to check.
 * MSG  - The string literal of the error message to print only if EXPR evalutes
 *        to false.
 *
 * Always returns true. */
#define STATIC_ASSERT_EXPR(EXPR, MSG)   \
    (!!sizeof( struct { static_assert ( (EXPR), MSG ); char c; } ))

/**
 * Gets the number of elements of the given array. */
#define ARRAY_CARDINALITY(ARRAY) (             \
    sizeof(ARRAY) / sizeof(0[ARRAY])    \
    * STATIC_ASSERT_EXPR( IS_ARRAY(ARRAY), #ARRAY "must be an array" ))

Sample use-case for IS_C_STR():

/**
 * Gets the length of S.
 *
 * S - The C string literal to get the length of. */
#define STRLITLEN(S) \
    (ARRAY_CARDINALITY(S) - STATIC_ASSERT_EXPR(IS_C_STR(S), \
    #S " must be a C string literal"))

The above would work for:

char s[] = "hello";
size_t slen = STRLITLEN(s);

and:

size_t tlen = STRLITLEN("hello");

But not for this:

extern char u[];
size_t ulen = STRLITLEN(s);  // error

or this:

char const *const s = "hello";
size_t len = STRLITLEN(s);

Both would result in a compilation error.

Code:

#ifndef TRAITS_H 
#define TRAITS_H 1

#include <assert.h>
#include <complex.h>
#include <stdio.h>

/**
 * Checks (at compile-time) if an expression is compatible with a type.
 *
 * EXPR - An expression. It is not evaluted.
 * T    - The type to check against.
 *
 * Note: Only an expression can be compared with a type. Two expressions or 
 * two type names can not be directly compared. 
 *
 * To compare two types, a compound literal can be used to create a literal of
 * a given type like so:
 *     
 *     IS_COMPATIBLE((size_t){0}, unsigned long);
 *
 * To test two variables for type compatibility, typeof can be used like so:
 *     
 *     IS_COMPATIBLE(x, typeof(y));
 *
 * Also note that this would not work for arrays, nor when one argument is a
 * pointer and another an array.
 *
 * Returns to 1 (true) if EXPR is compatible with T, 0 (false) elsewise. */
#define IS_COMPATIBLE(EXPR, T) \
    _Generic((EXPR),           \
        T      : 1,            \
        default: 0             \
    )     

/**
 * Checks (at compile-time) if T has type nullptr_t.
 *
 * T - An expression. It is not evaluted. 
 *
 * Returns 1 (true) if T is the type nullptr_t, 0 (false) elsewise. */
#define IS_NULLPTR(T) \
    _Generic((T),     \
        nullptr_t: 1, \
        default  : 0  \
    )

/**
 * Checks (at compile-time) if T has type FILE *.
 *
 * T - An expression. It is not evaluted. 
 *
 * Returns 1 (true) if T is the type FILE *, 0 (false) elsewise. */
#define IS_FILE_PTR(T) \
    _Generic((T),      \
        FILE * : 1,    \
        default: 0     \
    )

/**
 * Checks (at compile-time) whether T is an array.
 *
 * T - An expression. It is not evaluted. 
 *
 * Note: IS_ARRAY() distinguishes between arrays and pointers, not between
 *       arrays and arbitrary other types.
 * 
 * Returns 1 (true) only if T is an array; 0 (false) elsewise. 
 *
 * See also: https://stackoverflow.com/a/77881417/99089 */
#define IS_ARRAY(T)                 \
    _Generic( &(T),                 \
        typeof(*T) (*)[]    : 1,    \
        default             : 0     \
    )

/**
 * Checks (at compile-time) whether A is a pointer.
 *
 * T - An expression. It is not evaluted. 
 *
 * Note: IS_POINTER() distinguishes between arrays and pointers, not between
 *       pointers and arbitrary other types.
 * 
 * Returns 1 (true) only if T is a pointer; 0 (false) elsewise. 
 *
 * See also: https://stackoverflow.com/a/77881417/99089 */
#define IS_POINTER(T)   !IS_ARRAY(T)

/**
 * Implements a "static if" similar to "if constexpr" in C++.
 *
 * EXPR - An expression (evaluated at compile-time).
 * THEN - An expression returned only if EXPR is non-zero (true).
 * ELSE - An expression returned only if EXPR is zero (false).
 *
 * Returns:
 *     THEN only if EXPR is non-zero (true); or:
 *     ELSE only if EXPR is zero (false). */
#define STATIC_IF(EXPR, THEN, ELSE)     \
    _Generic( &(char[1 + !!(EXPR)]){0}, \
        char (*)[2]: (THEN),            \
        char (*)[1]: (ELSE)             \
    )

/**
 * Checks (at compile-time) whether char is signed or unsigned.
 *
 * Returns 1 (true) if char is signed, else 0 (false). */
#define IS_CHAR_SIGNED  STATIC_IF((char)-1 < 0, 1, 0)

/**
 * Checks (at compile-time) whether the type of T is a signed type. 
 *
 * T - An expression. It is not evaluated. 
 *
 * Note: This would not detect _BitInt. 
 *
 * Returns 1 (true) only if T is a signed type; 0 (false) elsewise. */
#define IS_SIGNED(T)                    \
    _Generic((T),                       \
        char         : IS_CHAR_SIGNED,  \
        short int    : 1,               \
        int          : 1,               \
        long         : 1,               \
        long long    : 1,               \
        default      : 0                \
    )

/**
 * Checks (at compile-time) whether the type of T is an unsigned type.
 *
 * T - An expression. It is not evaluated. 
 *
 * Note: This would not detect _BitInt. 
 *
 * Returns 1 (true) only if T is an unsigned type; 0 (false) elsewise. */
#define IS_UNSIGNED(T)                            \
    _Generic((T),                                 \
        _Bool                  : 1,               \
        char                   : !IS_CHAR_SIGNED, \
        unsigned char          : 1,               \
        unsigned short int     : 1,               \
        unsigned int           : 1,               \
        unsigned long int      : 1,               \
        unsigned long long int : 1,               \
        default                : 0                \
    )

/**
 * Checks (at compile-time) whether the type of T is any integral type. 
 *
 * T - An expression. It is not evaluated. 
 *
 * Note: This would not detect _BitInt.
 * 
 * Returns 1 (true) if T is any integral type, 0 (false) elsewise. */
#define IS_INTEGRAL(T)  (IS_SIGNED(T) || IS_UNSIGNED(T))

/**
 * Checks (at compile-time) whether the type of T is a floating-point type.
 *
 * T - An expression. It is not evaluated. 
 *
 * Returns 1 (true) if T is a floating-point type, 0 (false) elsewise. */
#if defined(__STDC_IEC_60559_DFP__)         \
    && defined(__STDC_IEC_60559_COMPLEX__)  \
    && defined(_Imaginary_I)
    #define IS_FLOATING_POINT(T)            \
        _Generic((T),                       \
            float                 : 1,      \
            double                : 1,      \
            long double           : 1,      \
            float _Complex        : 1,      \
            double _Complex       : 1,      \
            long double _Complex  : 1,      \
            float _Imaginary      : 1,      \
            double _Imaginary     : 1,      \
            long double _Imaginary: 1,      \
            _Decimal32            : 1,      \
            _Decimal64            : 1,      \
            _Decimal128           : 1,      \
            default               : 0       \
        )
#elif defined(__STDC_IEC_60559_COMPLEX__)   \
      && defined(_Imaginary_I)              \
      && !defined(__STDC_IEC_60559_DFP__)
    #define IS_FLOATING_POINT(T)            \
        _Generic((T),                       \
            float                 : 1,      \
            double                : 1,      \
            long double           : 1,      \
            float _Complex        : 1,      \
            double _Complex       : 1,      \
            long double _Complex  : 1,      \
            float _Imaginary      : 1,      \
            double _Imaginary     : 1,      \
            long double _Imaginary: 1,      \
            default               : 1       \
        )
#elif defined(__STDC_IEC_60559_COMPLEX__)   \
      && defined(__STDC_IEC_60559_DFP__)    \
      && !defined(_Imaginary_I)
    #define IS_FLOATING_POINT(T)            \
        _Generic((T),                       \
            float                 : 1,      \
            double                : 1,      \
            long double           : 1,      \
            float _Complex        : 1,      \
            double _Complex       : 1,      \
            long double _Complex  : 1,      \
            _Decimal32            : 1,      \
            _Decimal64            : 1,      \
            _Decimal128           : 1,      \
            default               : 0       \
        )
#elif defined(__STDC_IEC_60559_DFP__) && !defined(__STDC_IEC_60559_COMPLEX__)
    #define IS_FLOATING_POINT(T)            \
        _Generic((T),                       \
            float               : 1,        \
            double              : 1,        \
            long double         : 1,        \
            _Decimal32          : 1,        \
            _Decimal64          : 1,        \
            _Decimal128         : 1,        \
            default             : 0         \
        )
#elif defined(__STDC_IEC_60559_COMPLEX__) && !defined(__STDC_IEC_60559_DFP__)
    #define IS_FLOATING_POINT(T)            \
        _Generic((T),                       \
            float                 : 1,      \
            double                : 1,      \
            long double           : 1,      \
            float _Complex        : 1,      \
            double _Complex       : 1,      \
            long double _Complex  : 1,      \
            default               : 0       \
        )
#else
    #define IS_FLOATING_POINT(T)            \
        _Generic((T),                       \
            float      : 1,                 \
            double     : 1,                 \
            long double: 1,                 \
            default    : 0                  \
        )
#endif

/**
 * Checks (at compile-time) whether the type of T is any arithmetic type. 
 *
 * T - An expression. It is not evaluated. 
 *
 * Note: This would not detect _BitInt.
 *
 * Returns 1 (true) only if T is a C is any arithmetic type, 0 (false) elsewise. */
#define IS_ARITHMETIC(T)    (IS_INTEGRAL(T) || IS_FLOATING_POINT(T))

/**
 * Checks (at compile-time) whether the type of T is a C string type, i.e.
 * char *, or char const *.
 *
 * T - An expression. It is not evaluated.
 *
 * Returns 1 (true) only if T is a C string type, 0 (false) elsewise. */
#define IS_C_STR(T)         \
    _Generic((T),           \
        char *      : 1,    \
        char const *: 1,    \
        default     : 0     \
    )

/** 
 * Checks (at compile-time) whether the type of T is compatible with the type
 * of an array of length N.
 *
 * T - An expression. It is not evaluated.
 * N - Length of array.
 *
 * Returns 1 (true) only if T is compatible with array of length N, 0 (false)
 * elsewise. */
#define IS_COMPATIBLE_WITH_ARRAY_OF_LENGTH_N(T, N)  \
    _Generic(&(T),                                  \
        typeof(*T) (*)[N]: 1,                       \
        default          : 0                        \
    )

/**
 * Checks (at compile-time) whether the type of T is variable-length array or
 * an unspecified-length array.
 *
 * T - An expression. It is not evaluated.
 *
 * Returns 1 (true) only if T is a VLA or a ULA, 0 (false) elsewise.
 *
 * See also: https://stackoverflow.com/a/78597305/20017547 */
#define IS_VLA_OR_ULA(T)                            \
    (IS_COMPATIBLE_WITH_ARRAY_OF_LENGTH_N(T, 1)     \
    && IS_COMPATIBLE_WITH_ARRAY_OF_LENGTH_N(T, 2))

/**
 * Checks (at compile-time) whether the type of T is a function type.
 *
 * T - An expression. It is not evaluated.
 *
 * Returns 1 (true) only if T is a function type, 0 (false) elsewise. 
 *
 * See also: https://stackoverflow.com/a/78601265/20017547 */
#define IS_FUNCTION(T)      \
    _Generic((T),           \
        typeof(T)*: true,   \
        default:    false   \
    )

#endif  /* TRAITS_H */

Tests:

#include <complex.h>
#include <stdio.h>
#include <stdlib.h>

#include "traits.h"

/* Current versions of gcc and clang support -std=c2x which sets 
 * __STDC_VERSION__ to this placeholder value. GCC 14.1 does not set
 * __STDC_VERSION__ to 202311L with the std=c23 flag, but Clang 18.1 does. */
#define C23_PLACEHOLDER 202000L
    
#if defined(__STDC_VERSION__) && __STDC_VERSION >= C23_PLACEHOLDER
    #define NORETURN    [[noreturn]]
#elif defined(_MSC_VER)
    #define NORETURN    __declspec(noreturn)
#elif defined(__GNUC__) || defined(__clang__) || defined(__INTEL_LLVM_COMPILER)
    #define NORETURN    __attribute__((noreturn))
#else
    #define NORETURN    _Noreturn
#endif

NORETURN static void cassert(const char cond[static 1], 
                             const char file[static 1],
                             size_t line)
{
    fflush(stdout);
    fprintf(stderr, "Assertion failed: '%s' at %s, line %zu.\n", cond, file, line);
    exit(EXIT_FAILURE);
}

#define test(cond) do { \
    if (!(cond)) { cassert(#cond, __FILE__, __LINE__); } } while (false)

int func(int)
{
    return 0;
}

void test_is_compatible(void)
{
    int *i;

    test(IS_COMPATIBLE(i, int *));
    test(!IS_COMPATIBLE(i, float *));

    int (*f)(int); 
    test(IS_COMPATIBLE(func, int (*)(int)));
    test(IS_COMPATIBLE(f, int (*)(int)));
    test(IS_COMPATIBLE(func, typeof(f)));
    test(!IS_COMPATIBLE(func, void (*)(int)));

    struct A { int x; int y; } A;
    struct B { double a; double b; } B;

    test(IS_COMPATIBLE(A, struct A));
    test(IS_COMPATIBLE(B, struct B));
    test(!IS_COMPATIBLE(A, struct B));
    test(!IS_COMPATIBLE(B, struct A));

    typedef const char *string;
    typedef int VAL;

    string greeting; 
    VAL a; 
    
    test(IS_COMPATIBLE(greeting, string));
    test(!IS_COMPATIBLE(greeting, char *));
    test(IS_COMPATIBLE(a, int));
    test(IS_COMPATIBLE(a, VAL));
    test(IS_COMPATIBLE((VAL){10}, int));
    test(IS_COMPATIBLE((int){10}, VAL));
    test(IS_COMPATIBLE(a, typeof((int){10})));
}

void test_is_nullptr(void)
{
    char *c = nullptr;
    test(IS_NULLPTR(nullptr));
    test(!IS_NULLPTR(NULL));
    test(!IS_NULLPTR(c));
}

void test_is_file_ptr(void)
{
    FILE *f; 
    test(IS_FILE_PTR(f));
    test(!IS_FILE_PTR(0));
    test(!IS_FILE_PTR(NULL));
}

void test_is_array(void)
{
    int n = 5;
    char VLA[n];
    int FLA[10];
    extern char ULA[];

    int *p = FLA;
    char *c; 

    test(IS_ARRAY(VLA));
    test(IS_ARRAY(FLA));
    test(IS_ARRAY(ULA));

    test(!IS_ARRAY(p));
    test(!IS_ARRAY(c));
}

void test_is_pointer(void)
{
    int n = 5;
    char VLA[n];
    int FLA[10];
    extern char ULA[];

    int *p = FLA;
    char *c;

    test(!IS_POINTER(VLA));
    test(!IS_POINTER(FLA));
    test(!IS_POINTER(ULA));

    test(IS_POINTER(p));
    test(IS_POINTER(c));
}

void test_is_signed(void)
{
    test(IS_CHAR_SIGNED ? IS_SIGNED((char){0}) : !IS_SIGNED((char){0}));

    test(!IS_SIGNED((unsigned char){0}));
    test(!IS_SIGNED((unsigned int){0}));
    test(!IS_SIGNED((unsigned long int){0}));
    test(!IS_SIGNED((unsigned long long int){0}));

    test(IS_SIGNED((short int){0}));
    test(IS_SIGNED((int){0}));
    test(IS_SIGNED((long int){0}));
    test(IS_SIGNED((long long int){0}));
}

void test_is_unsigned(void)
{
    test(IS_CHAR_SIGNED ? !IS_UNSIGNED((char){0}) : IS_UNSIGNED((char){0}));

    test(!IS_UNSIGNED((char){0}));
    test(!IS_UNSIGNED((short int){0}));
    test(!IS_UNSIGNED((int){0}));
    test(!IS_UNSIGNED((long int){0}));
    test(!IS_UNSIGNED((long long int){0}));

    test(IS_UNSIGNED((_Bool){0}));
    test(IS_UNSIGNED((unsigned char){0}));
    test(IS_UNSIGNED((unsigned int){0}));
    test(IS_UNSIGNED((unsigned long int){0}));
    test(IS_UNSIGNED((unsigned long long int){0}));
}

void test_is_integral(void)
{
    test(IS_INTEGRAL((_Bool){0}));
    test(IS_INTEGRAL((char){0}));
    test(IS_INTEGRAL((unsigned char){0}));
    test(IS_INTEGRAL((short){0}));
    test(IS_INTEGRAL((int){0}));
    test(IS_INTEGRAL((long){0}));
    test(IS_INTEGRAL((long long){0}));
    test(IS_INTEGRAL((unsigned int){0}));
    test(IS_INTEGRAL((unsigned long){0}));
    test(IS_INTEGRAL((unsigned long long){0}));

    test(!IS_INTEGRAL((float){0}));
    test(!IS_INTEGRAL((double){0}));
    test(!IS_INTEGRAL((long double){0}));
}

void test_is_floating_point(void)
{
    test(IS_FLOATING_POINT((float){0}));
    test(IS_FLOATING_POINT((double){0}));
    test(IS_FLOATING_POINT((long double){0}));

#ifdef __STDC_IEC_60559_DFP__
    test(IS_FLOATING_POINT((_Decimal32) {0}));
    test(IS_FLOATING_POINT((_Decimal64) {0}));
    test(IS_FLOATING_POINT((_Decimal128) {0}));
#endif

#ifdef __STDC_IEC_60559_COMPLEX__
    test(IS_FLOATING_POINT((float _Complex){0}));
    test(IS_FLOATING_POINT((double _Complex){0}));
    test(IS_FLOATING_POINT((long double _Complex){0}));
#endif

#ifdef _Imaginary_I
    test(IS_FLOATING_POINT((float _Imaginary){0}));
    test(IS_FLOATING_POINT((double _Imaginary){0}));
    test(IS_FLOATING_POINT((long double _Imaginary){0}));
#endif

    test(!IS_FLOATING_POINT((int){0}));
    test(!IS_FLOATING_POINT((unsigned int){0}));
}

void test_is_arithmetic(void)
{
    test(IS_ARITHMETIC((_Bool){0}));
    test(IS_ARITHMETIC((char){0}));
    test(IS_ARITHMETIC((unsigned char){0}));
    test(IS_ARITHMETIC((short){0}));
    test(IS_ARITHMETIC((int){0}));
    test(IS_ARITHMETIC((long){0}));
    test(IS_ARITHMETIC((long long){0}));
    test(IS_ARITHMETIC((unsigned int){0}));
    test(IS_ARITHMETIC((unsigned long){0}));
    test(IS_ARITHMETIC((unsigned long long){0}));
    test(IS_ARITHMETIC((float){0}));
    test(IS_ARITHMETIC((double){0}));
    test(IS_ARITHMETIC((long double){0}));

#ifdef __STDC_IEC_60559_DFP__
    test(IS_ARITHMETIC((_Decimal32){0}));
    test(IS_ARITHMETIC((_Decimal64){0}));
    test(IS_ARITHMETIC((_Decimal128){0}));
#endif

#ifdef __STDC_IEC_60559_COMPLEX__
    test(IS_ARITHMETIC((float _Complex){0}));
    test(IS_ARITHMETIC((double _Complex){0}));
    test(IS_ARITHMETIC((long double _Complex){0}));
#endif

#ifdef _Imaginary_I
    test(IS_FLOATING_POINT((float _Imaginary){0}));
    test(IS_FLOATING_POINT((double _Imaginary){0}));
    test(IS_FLOATING_POINT((long double _Imaginary){0}));
#endif

    test(!IS_ARITHMETIC(NULL));
    test(!IS_ARITHMETIC(nullptr));

    char c[] = {'1', '2', '3'};
    test(!IS_ARITHMETIC(c));
}

void test_is_c_str(void)
{
    char *str1;
    char *const str2;
    const char *str3;
    char str4[10]; 
    int *c;

    test(IS_C_STR(str1));
    test(IS_C_STR(str2));
    test(IS_C_STR(str3));
    test(IS_C_STR(str4));

    test(!IS_C_STR((int){0}));
    test(!IS_C_STR((double){0}));
    test(!IS_C_STR(c));

}

void test_is_compatible_with_array_of_length_n(void)
{
    char array[10];
    char array2[20];

    test(IS_COMPATIBLE_WITH_ARRAY_OF_LENGTH_N(array, 10));
    test(IS_COMPATIBLE_WITH_ARRAY_OF_LENGTH_N(array2, 20));

    test(!IS_COMPATIBLE_WITH_ARRAY_OF_LENGTH_N(array, 20));
    test(!IS_COMPATIBLE_WITH_ARRAY_OF_LENGTH_N(array2, 10));
}

void test_is_ula_or_vla(void)
{
    extern char ULA[];
    int x = 1;
    int VLA[x];
    int FLA[10];

    test(IS_VLA_OR_ULA(ULA));
    test(IS_VLA_OR_ULA(VLA));
    test(!IS_VLA_OR_ULA(FLA));
}

void test_is_function(void)
{
    int (*fptr)(void);
    int array[10];
    int x;

    test(IS_FUNCTION(test_is_function));
    test(IS_FUNCTION(test_is_ula_or_vla));

    test(!IS_FUNCTION(fptr));
    test(!IS_FUNCTION(array));
    test(!IS_FUNCTION(x));
    test(!IS_FUNCTION(nullptr));
}

int main(void)
{
    test_is_compatible();
    test_is_nullptr();
    test_is_file_ptr();
    test_is_array();
    test_is_pointer();
    test_is_signed();
    test_is_unsigned();
    test_is_integral();
    test_is_floating_point();
    test_is_arithmetic();
    test_is_c_str();
    test_is_compatible_with_array_of_length_n();
    test_is_ula_or_vla();
    test_is_function();
    return EXIT_SUCCESS;
}

Needless to say, all assertions passed. I could have used static_assert here albeit.

Review Request:

Pitfalls I have not yet realized or documented, wrong documentation, bugs, simplifications, wrong behavior, wrong tests, missing tests, et cetera.

Useful traits I can add.

\$\endgroup\$
3
  • 1
    \$\begingroup\$ minor naming nit: Instead of IS_VLA_OR_ULA, maybe IS_FIXED_SIZE_ARRAY, or IS_CONSTANT_... ? // To avoid parentheses, consider introducing each trait with: "Compile-time check of whether ..." // I like restricting it to C23, so odd things like ones-complement are off the table. \$\endgroup\$
    – J_H
    Commented 2 days ago
  • \$\begingroup\$ @J_H Are you speaking of defining IS_FIXED_SIZE_ARRAY(), IS_VARIABLE_LENGTH_ARRAY() et cetera, or of renaming IS_VLA_OR_ULA() to one of the aforementioned named. I doubt it is the latter, because VLA is a variable-length array, and ULA is an unspecified-length array. Unfortunately, it is not possible to distinguish between them, so we can't have separate IS_VLA() and IS_ULA(). \$\endgroup\$
    – Harith
    Commented 2 days ago
  • 1
    \$\begingroup\$ I was only going after the general "this_or_that" clunkiness. By negating the sense of the predicate, it seemed like we could speak of the concept using some defined term. And I was saying I don't care if we (or the spec?) call it a "fixed" size, a "constant" size, whatever. I'm just looking for something that is both precise and is human-friendly as your're scanning the source code. It's just easier to think about "I have a this", rather than "I have this or that." \$\endgroup\$
    – J_H
    Commented 2 days ago

2 Answers 2

3
\$\begingroup\$

Standard types not all covered

IS_UNSIGNED((size_t)0) returns 0 when size_t is not one of the other standard unsigned types. size_t may be another unsigned type. It has been this way since at least C99.

Similar issues apply to ptrdiff_t, (u)intmax_t and perhaps more.

Although _Generic does not allow two of the same type, via default, code can do so to handle these uncommon cases

#define IS_UNSIGNED2(T)                           \
    _Generic((T),                                 \
        uintmax_t              : 1,               \
        default                : 0                \
    )

#define IS_UNSIGNED1(T)                           \
    _Generic((T),                                 \
        size_t                 : 1,               \
        default                : IS_UNSIGNED2(T)  \
    )

#define IS_UNSIGNED(T)                            \
    _Generic((T),                                 \
        _Bool                  : 1,               \
        char                   : !IS_CHAR_SIGNED, \
        unsigned char          : 1,               \
        unsigned short int     : 1,               \
        unsigned int           : 1,               \
        unsigned long int      : 1,               \
        unsigned long long int : 1,               \
        default                : IS_UNSIGNED1(T)  \
    )

We can extend such to cope with uintN_t, uint_fastN_t, uint_leastN_t but this all gets a bit silly after coping with the usual suspects.

time_t

Note that time_t and clock_t are real types, not certainly integer types.

Steering code with IS_INTEGRAL((time_t)0), IS_SIGNED((time_t)0), ... is a real world example of how these macros can be used.

\$\endgroup\$
6
  • 1
    \$\begingroup\$ @Harith Not necessarily extend, maybe a 24 or 48 bit unsigned. \$\endgroup\$ Commented yesterday
  • 1
    \$\begingroup\$ I see that clock_t is an arithmetic type as well. Does the ISO C Standard have a specific list of all the types? \$\endgroup\$
    – Harith
    Commented yesterday
  • 1
    \$\begingroup\$ Yes clock_t is some real type, commonly some integer, but could be FP. "specific list" --> I have found no separate list, just inferences given the entire spec. With C23, I think the genie is out of the bottle concerning a bounded type list. Just can't wait to try out uint65536_t. \$\endgroup\$ Commented yesterday
  • 2
    \$\begingroup\$ @chux-ReinstateMonica Will uint65536_t be available as uint0xFFFF_t? And an octal version, too, for us oldies? :-) Can't tell you how long I've been waiting for uint007_t to tally all the nefarious plots that James Bond thwarted. :-) \$\endgroup\$
    – Fe2O3
    Commented yesterday
  • 1
    \$\begingroup\$ @Fe2O3 No. \$\endgroup\$ Commented yesterday
0
\$\begingroup\$

This answer will expand on the problem of missing standard types highlighted in @chux's answer.

All references will be from ISO/IEC 9899:2023 - N3220 working draft.


time_t and clock_t:

From 7.29.1, Components of time:

4 The types declared are size_t (described in 7.21);

clock_t

and

time_t

which are real types capable of representing times;

From 6.2.5, Types:

23 Integer and floating types are collectively called arithmetic types. Each arithmetic type belongs to one type domain: the real type domain comprises the real types, the complex type domain comprises the complex types.

Also:

14 14 standard floating types and the decimal floating types are collectively called the real floating types..

Do note that the standard does not say anything about complex integer types, so all integer types defined in the Standard belong to the real type domain.

I am unsure where time_t and clock_t fit in the current traits.


sig_atomic_t:

From 7.14, Signal handling <signal.h>:

2 The type defined is

sig_atomic_t

which is the (possibly volatile-qualified) integer type of an object that can be accessed as an atomic entity, even in the presence of asynchronous interrupts.

So IS_INTEGRAL() should return 1 (true) for sig_atomic_t. As the signedness is not specified (like plain char), code could use:

#define IS_SIG_ATOMIC_T_SIGNED    STATIC_IF((sig_atomic_t) -1 < 0, 1, 0)

and then for IS_SIGNED():

_Generic((T),                              \
    sig_atomic_t: IS_SIG_ATOMIC_T_SIGNED,  \
    ...

And for IS_UNSIGNED():

_Generic((T),                              \
    sig_atomic_t: !IS_SIG_ATOMIC_T_SIGNED, \
    ...

ptrdiff_t, size_t, and wchar_t:

From 7.21 Common definitions <stddef.h>:

3 The types are

ptrdiff_t

which is the signed integer type of the result of subtracting two pointers;

size_t

which is the unsigned integer type of the result of the sizeof operator; ...

wchar_t

which is an integer type whose range of values can represent distinct codes for all members of the largest extended character set specified among the supported locales; the null character shall have the code value zero.

ptrdiff_t is a signed integer type, and size_t is an unsigned integer type. As of wchar_t, its signedness is not specified. Code should use the same method as was shown above for sig_atomic_t.

Exact-width integer types:

These are the usual (signed):

  • int8_t
  • int16_t
  • int32_t
  • int64_t

and (unsigned):

  • uint8_t
  • uint16_t
  • uint32_t
  • uint64_t

But 7.22.1.1, Exact-width integer types only mentions int8_t and uint24_t.

Minimum-width integer types:

From 7.22.1.2, Minimum-width integer types:

4 The following types are required: int_least8_t int_least16_t int_least32_t int_least64_t uint_least8_t uint_least16_t uint_least32_t uint_least64_t

The former four being signed, and the latter four being unsigned.

Fastest minimum-width integer types:

From 7.22.1.3, Fastest minimum-width integer types:

3 The following types are required: int_fast8_t int_fast16_t int_fast32_t int_fast64_t uint_fast8_t uint_fast16_t uint_fast32_t uint_fast64_t

The former four being signed, and the latter four being unsigned.

IS_SIGNED() and IS_UNSIGNED() should be updated to take these into account.

The crucial thing to note about uint_leastN_t types and uint_fastN_t types is that they can be compatible amongst themselves. For instance, uint_fast32_t may be in fact 64 bits long, and same as uint_fast64_t, in which you would get a compilation error for having two type-names in the association-list with compatible types.

So 4 IS_SIGNEDN() macros cases would be required to detect int_fastN_t types, and 4 more IS_SIGNEDN() macros would be required to detect int_leastN_t() types. And then 8 IS_UNSIGNEDN() macros would be required to detect all uint_fastN_t and uint_leastN_t types. All this can be avoided with judicious use of INT_FASTN_MAX, UINT_FASTN_MAX, INT_LEASTN_MAX, and UINT_LEASTN_MAX. But empirically, the code is more readable and simpler without them.

intptr_t and uinptr_t:

From 7.22.1.4, Integer types capable of holding object pointers:

The following type designates a signed integer type, other than a bit-precise integer type, with the property that any valid pointer to void can be converted to this type, then converted back to pointer to void, and the result will compare equal to the original pointer:

intptr_t

The following type designates an unsigned integer type, other than a bit-precise integer type, with the property that any valid pointer to void can be converted to this type, then converted back to pointer to void, and the result will compare equal to the original pointer:

uintptr_t

These types are optional.

So intptr_t is a signed integer type and uintptr_t is an unsigned integer type. IS_SIGNED() and IS_UNSIGNED() should be updated to take these into account.

intmax_t and uintmax_t:

The following type designates a signed integer type, other than a bit-precise integer type, capable of representing any value of any signed integer type with the possible exceptions of signed bit-precise integer types and of signed extended integer types that are wider than long long and that are referred by the type definition for an exact width integer type:

intmax_t

The following type designates the unsigned integer type that corresponds to intmax_t: 307)

uintmax_t

These types are required.

So intmax_t is a signed integer type, and uintmax_t is an unsigned integer type. IS_SIGNED() and IS_UNSIGNED() should be updated to take these into account.

char8_t, char16_t, and char32_t:

From 7.30, Unicode utilities <uchar.h>:

The types declared are mbstate_t (described in 7.31.1) and size_t (described in 7.21);

char8_t

which is an unsigned integer type used for 8-bit characters and is the same type as unsigned char;

char16_t

which is an unsigned integer type used for 16-bit characters and is the same type as uint_least16_t (described in 7.22.1.2); and

char32_t

which is an unsigned integer type used for 32-bit characters and is the same type as uint_least32_t (also described in 7.22.1.2).

No extra checks are required for these, assuming cases are present for unsigned char, uint_least16_t, and uint_least32_t.

wint_t:

From 7.31, Extended multibyte and wide character utilities <wchar.h>:

...

wint_t

which is an integer type unchanged by default argument promotions that can hold any value corresponding to members of the extended character set, as well as at least one value that does not correspond to any member of the extended character set (see subsequent WEOF description);386)

So wint_t is an integer type whose signedness is not specified. Code should use the same method to detect its signedness as was shown above for sig_atomic_t.

\$\endgroup\$
1
  • \$\begingroup\$ From what I have ended up with currently, 16 IS_SIGNEDN() macros are required to detect all signed types, and 16 IS_UNSIGNED() macros are required to detect all unsigned types. \$\endgroup\$
    – Harith
    Commented yesterday

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.