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_SIGNEDIS_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.
Edit: The idea came from https://dev.to/pauljlucas/generic-in-c-i48, where I took some code from.
IS_FIXED_SIZE_ARRAY(),IS_VARIABLE_LENGTH_ARRAY()et cetera, or of renamingIS_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 separateIS_VLA()andIS_ULA(). \$\endgroup\$cdecland StackOverflow. Were they not to be freely used? I can take down my post if you wish. \$\endgroup\$