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.

Edit: The idea came from https://dev.to/pauljlucas/generic-in-c-i48, where I took some code from.

\$\endgroup\$
7
  • 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
    CommentedJun 10, 2024 at 19:44
  • \$\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\$CommentedJun 11, 2024 at 0:23
  • 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
    CommentedJun 11, 2024 at 0:37
  • 1
    \$\begingroup\$Much of "your" code seems like plagiarism from my blog post.\$\endgroup\$CommentedJul 8, 2024 at 21:06
  • \$\begingroup\$@PaulJ.Lucas Yes, I found the initial traits there, and then some from cdecl and StackOverflow. Were they not to be freely used? I can take down my post if you wish.\$\endgroup\$CommentedJul 9, 2024 at 0:36

3 Answers 3

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\$
    – chux
    CommentedJun 11, 2024 at 18:17
  • 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\$CommentedJun 11, 2024 at 18:39
  • 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\$
    – chux
    CommentedJun 11, 2024 at 18:46
  • 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
    CommentedJun 11, 2024 at 21:50
  • 1
    \$\begingroup\$@Fe2O3 No.\$\endgroup\$
    – chux
    CommentedJun 12, 2024 at 5:41
2
\$\begingroup\$

Missing Extended Types

Implementations are allowed to provide extended integer types, with __int128 being a common one, and these macros fail for them. On an ILP64 system, none of _Bool, char, short, int, long nor long long are exactly 32 bits wide, and at least one compiler from Cray had a setting where short was 64-bit as well. Edit: C23 specifies that the conversion performed on the controlling expression is lvalue conversion, which drops qualifiers, so you wouldn’t need to worry about promotion to signed int as I originally thought. However, there are compilers where int32_t and uint32_t are extended types, and this implementation of IS_SIGNED or IS_UNSIGNED would fail.

C23 makes a strong guarantee about which exact-width types, such as int16_t, shall be defined as typedef names in <stdint.h>:

If an implementation provides standard or extended integer types with a particular width and no padding bits, it shall define the corresponding typedef names.

C99 makes a weaker, but still useful, one:

if an implementation provides integer types with widths of 8, 16, 32, or 64 bits, no padding bits, and (for the signed types) that have a two's complement representation, it shall define the corresponding typedef names.

This means you can check for the existence of the exact-width type of each width you care about, with e.g. #ifdef INT8_MAX, enumerate every relevant width, and be virtually guaranteed to catch a typedef of every built-in and extended type. (You can even test for int18_t or int36_t if portability to 60-year-old hardware is a concern.)

The one problem here is that at least two of short, int, long and long long are almost always the same width (and this is also true of char on many word-addressed machines), and there is no way to tell which one the exact-width type is a typedef of. To code defensively around this, you can add clauses for each of the standard types, and then test for every possible extended exact-width type that is not the same width as any standard type, for instance:

#if defined(INT32_MAX) && \ (INT32_MAX != CHAR_MAX) && \ (INT32_MAX != SHRT_MAX) && \ (INT32_MAX != INT_MAX) && \ (INT32_MAX != LONG_MAX) // LLONG_MAX must be at least +9'223'372'036'854'775'807. int32_t : 1, #endif 

This will add a backup case for int32_t on implementations where no standard type is 32 bits wide, but an extended type is, and suppress it in all other cases. If you’re writing a bunch of these, you’d use a helper macro such as

/* Intended for checks such as * #if defined(INT16_MAX) && IS_NOT_STD_SIGNED_MAX(INT16_MAX) * Warning: could evaluate the argument more than once. */ #define IS_NOT_STD_SIGNED_MAX(max) \ (((max) != CHAR_MAX && \ (max) != SHRT_MAX && \ (max) != INT_MAX && \ (max) != LONG_MAX && \ (max) != LLONG_MAX ) \ ? 1 : 0) 

One gotcha: C23 allows there to be exact-width integer types wider than intmax_t, and previous standards did not, which forced some compilers that supported __int128 not to have int128_t.

\$\endgroup\$
6
  • \$\begingroup\$Re: "Usually this makes the macros spuriously return false, but IS_SIGNED would spuriously return true for any unsigned extended type narrower than int, thanks to the default integer promotions." I did not understand. Where would the default integer promotions happen in IS_SIGNED?\$\endgroup\$CommentedJul 9, 2024 at 11:02
  • \$\begingroup\$@Harith Let’s say you pass a 32-bit unsigned rvalue to IS_SIGNED on an ILP64 system where neither char, short, int, long nor long long are 32 bits wide. Since it’s an integer type with lower rank than int whose values can all be represented by int, the integer promotions convert it implicitly to a signed int, and IS_SIGNED would evaluate it to 1.\$\endgroup\$
    – Davislor
    CommentedJul 9, 2024 at 11:59
  • \$\begingroup\$Does the documentation for _Generic specify that? Because as far as I know, it does not default promote the controlling expression, and would fail to compile if there is no matching or default case.\$\endgroup\$CommentedJul 9, 2024 at 13:31
  • \$\begingroup\$@Harith The integer promotions apply “in an expression wherever an int or unsigned int may be used” (§6.3.1.1 ¶2). The _Generic expression selects a clause with “a type name that is compatible with the type of the controlling expression.” So, yes.\$\endgroup\$
    – Davislor
    CommentedJul 9, 2024 at 17:37
  • 1
    \$\begingroup\$@Harith Thanks, some good answers there. C17 changed the wording about what conversion is performed on the controlling expression of a _Generic statement, to make it less ambiguous. Since C17/C23 specify “lvalue conversion,” which only drops type qualifiers, it looks like I am indeed wrong.\$\endgroup\$
    – Davislor
    CommentedJul 10, 2024 at 15:49
1
\$\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_tint_least16_tint_least32_tint_least64_tuint_least8_tuint_least16_tuint_least32_tuint_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_tint_fast16_tint_fast32_tint_fast64_tuint_fast8_tuint_fast16_tuint_fast32_tuint_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\$
4
  • \$\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\$CommentedJun 12, 2024 at 5:26
  • 1
    \$\begingroup\$"But 7.22.1.1, Exact-width integer types only mentions ..." --> note that C11's "These types are optional. However, if an implementation provides integer types with widths of 8, 16, 32, or 64 bits, no padding bits, and (for the signed types) that have a two’s complement representation, it shall define the corresponding typedef names." is replaced with C23's "If an implementation provides standard or extended integer types with a particular width and no padding bits, it shall define the corresponding typedef names."\$\endgroup\$
    – chux
    CommentedJun 14, 2024 at 12:52
  • 1
    \$\begingroup\$Also, wctrans_t and wctype_t are scalars. Types that are often scalars but don’t have to be include fpos_t, mbstate_t and the identifiers in <threads.h>.\$\endgroup\$
    – Davislor
    CommentedJul 9, 2024 at 8:45
  • 1
    \$\begingroup\$Additionally, if FLT_EVAL_METHOD in <math.h> is defined as something other than 0, 1 or 2, the types float_t and double_t could be floating-point types different from float, double or long double, and possibly each other.\$\endgroup\$
    – Davislor
    CommentedJul 9, 2024 at 9:21

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.