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\$