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.
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\$cdecl
and StackOverflow. Were they not to be freely used? I can take down my post if you wish.\$\endgroup\$