2

I need to write a C macro that checks to ensure all parameters passed to it are unsigned and of the same integer type. Ex: all input params are uint8_t, or all uint16_t, or all uint32_t, or all uint64_t.

Here is how this type of checking can be done in C++: Use static_assert to check types passed to macro

Does something similar exist in C, even if only by way of a gcc extension?

Note that static asserts are available in gcc via _Static_assert. (See my answer here: Static assert in C).

This fails to work:

int a = 1; 
int b = 2;
_Static_assert(__typeof__ a == __typeof__ b, "types don't match");

Error:

main.c: In function ‘main’:
main.c:23:20: error: expected expression before ‘__typeof__’
     _Static_assert(__typeof__ a == __typeof__ b, "types don't match");

UPDATE:

Here's precisely how to do what I want in C++ (using a function template, static_assert, and the <type_traits> header file). I needed to learn this anyway, for comparison purposes, so I just did. Run this code for yourself here: https://onlinegdb.com/r1k-L3HSL.

#include <stdint.h>
#include <stdio.h>
#include <type_traits> // std::is_same()

// Templates: https://www.tutorialspoint.com/cplusplus/cpp_templates.htm

// Goal: test the inputs to a "C macro" (Templated function in this case in C++) to ensure
// they are 1) all the same type, and 2) an unsigned integer type

// 1. This template forces all input parameters to be of the *exact same type*, even 
//    though that type isn't fixed to one type! This is because all 4 inputs to test_func()
//    are of type `T`.
template <typename T>
void test_func(T a, T b, T c, T d)
{
    printf("test_func: a = %u; b = %u; c = %u; d = %u\n", a, b, c, d);

    // 2. The 2nd half of the check: 
    // check to see if the type being passed in is uint8_t OR uint16_t OR uint32_t OR uint64_t!
    static_assert(std::is_same<decltype(a), uint8_t>::value ||
                  std::is_same<decltype(a), uint16_t>::value ||
                  std::is_same<decltype(a), uint32_t>::value ||
                  std::is_same<decltype(a), uint64_t>::value,
                  "This code expects the type to be an unsigned integer type\n"
                  "only (uint8_t, uint16_t, uint32_t, or uint64_t).");

    // EVEN BETTER, DO THIS FOR THE static_assert INSTEAD!
    // IE: USE THE TEMPLATE TYPE `T` DIRECTLY!
    static_assert(std::is_same<T, uint8_t>::value ||
                  std::is_same<T, uint16_t>::value ||
                  std::is_same<T, uint32_t>::value ||
                  std::is_same<T, uint64_t>::value,
                  "This code expects the type to be an unsigned integer type\n"
                  "only (uint8_t, uint16_t, uint32_t, or uint64_t).");
}

int main()
{
    printf("Begin\n");

    // TEST A: This FAILS the static assert since they aren't unsigned 
    int i1 = 10;
    test_func(i1, i1, i1, i1); 

    // TEST B: This FAILS to find a valid function from the template since 
    // they aren't all the same type 
    uint8_t i2 = 11;
    uint8_t i3 = 12;
    uint32_t i4 = 13;
    uint32_t i5 = 14;
    test_func(i2, i3, i4, i5);

    // TEST C: this works!
    uint16_t i6 = 15;
    uint16_t i7 = 16;
    uint16_t i8 = 17;
    uint16_t i9 = 18;
    test_func(i6, i7, i8, i9);

    return 0;
}

With just TEST A uncommented, you get this failure in the static assert since the inputs aren't unsigned:

main.cpp: In instantiation of ‘void test_func(T, T, T, T) [with T = int]’:
<span class="error_line" onclick="ide.gotoLine('main.cpp',46)">main.cpp:46:29</span>:   required from here
main.cpp:32:5: error: static assertion failed: This code expects the type to be an unsigned integer type
only (uint8_t, uint16_t, uint32_t, or uint64_t).
     static_assert(std::is_same<decltype(a), uint8_t>::value ||
     ^~~~~~~~~~~~~

with just TEST B uncommented, you get this failure to find a valid function from the template since the template expects all inputs to be the same type T:

main.cpp: In function ‘int main()’:
main.cpp:54:29: error: no matching function for call to ‘test_func(uint8_t&, uint8_t&, uint32_t&, uint32_t&)’
     test_func(i2, i3, i4, i5);
                             ^
main.cpp:26:6: note: candidate: template void test_func(T, T, T, T)
 void test_func(T a, T b, T c, T d)
      ^~~~~~~~~
main.cpp:26:6: note:   template argument deduction/substitution failed:
main.cpp:54:29: note:   deduced conflicting types for parameter ‘T’ (‘unsigned char’ and ‘unsigned int’)
     test_func(i2, i3, i4, i5);
                             ^

And with just TEST C uncommented, it passes and looks like this!

Begin
test_func: a = 15; b = 16; c = 17; d = 18

References:

  1. http://www.cplusplus.com/reference/type_traits/is_same/
  2. https://en.cppreference.com/w/cpp/types/is_same
  3. https://en.cppreference.com/w/cpp/language/decltype
  4. How do I restrict a template class to certain built-in types?

Related:

  1. Use static_assert to check types passed to macro [my own answer]
  2. Static assert in C [my own answer]
Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
  • C provides `__typeof__` (GNU C provides `typeof`) that allow you to query for type. See [Referring to a Type with typeof](https://gcc.gnu.org/onlinedocs/gcc/Typeof.html) – David C. Rankin Mar 10 '20 at 04:42
  • Yeah I thought about that but see no way to use it to compare types--only to copy them for instantiation. Ex: `int a; __typeof__ a b;` makes both `a` and `b` of type `int`. However, last I checked, `_Static_assert(__typeof__ a == __typeof__ b, "this failed");` doesn't seem to work at all. – Gabriel Staples Mar 10 '20 at 04:47
  • What I was thinking is like a `if (sizeof(__typeof__ a) == sizeof(__typeof__ b))` would distinguish between the `uintX_t` flavors. – David C. Rankin Mar 10 '20 at 04:49
  • That's true, but you don't even need a `__typeof__` for that though; just do: `if (sizeof(a) == sizeof(b))`, but that doesn't help me distinguish between `signed` and `unsigned` (ex: `int16_t` and `uint16_t`, both of which are 2 bytes), `float` or `double` and `uint32_t` on an 8-bit machine (all 3 of these types are 4-bytes in this case), or `double` and `uint64_t` on a 64-bit machine (both these types are 8 bytes), etc etc. In other words, `sizeof` *appears* to me to be ineffective and lack the necessary granulariy, unless someone knows a special way to use it I don't know. – Gabriel Staples Mar 10 '20 at 04:54
  • That is a quandary, because you can only use `__typeof__` where a `typedef` would be allowed -- which limits the value. I'm not aware of anything else that would get you there -- but I'm not going to rule out someone smarter knowing something that I don't in that department. It's generally not something you run across in C with it being so strongly typed. – David C. Rankin Mar 10 '20 at 05:00
  • @DavidC.Rankin I don't believe C provides for `__typeof__`. I could find no mention of it in the C11 working draft. I believe that link is merely explaining (in regards to `__typeof__` working for ISO C programs) that compiling with certain flags (such as `-ansi`) will disable the `typeof` keyword, whereas `__typeof__` would not be disabled. See here: https://gcc.gnu.org/onlinedocs/gcc/Alternate-Keywords.html – Christian Gibbons Mar 10 '20 at 06:07
  • @ChristianGibbons - you are correct. I've searched the standard and it doesn't mention `typeof`. So it won't be fully conforming. And yes, I did miss the alternate keyword implication. Thank you. – David C. Rankin Mar 10 '20 at 06:22
  • `__typeof__` is a gcc extension, but that's not a problem for me. I frequently rely on gcc extensions which are "non-conforming" to the C or C++ standards. `_Static_assert`, for example, is one of them. Eventually the standard comes around, 10 or 20 years later, and conforms to gcc. :) – Gabriel Staples Mar 10 '20 at 06:42

2 Answers2

3

If the most important aspect here is that you want it to fail to compile if a and b are different types, you can make use of C11's _Generic along with GCC's __typeof__ extension to manage this.

A generic example:

#include <stdio.h>

#define TYPE_ASSERT(X,Y) _Generic ((Y), \
    __typeof__(X): _Generic ((X), \
        __typeof__(Y): (void)NULL \
    ) \
)

int main(void)
{
    int a = 1; 
    int b = 2;
    TYPE_ASSERT(a,b);
    printf("a = %d, b = %d\n", a, b);
}

Now if we try to compile this code, it will compile fine and everybody is happy.

If we change the type of b to unsigned int, however, it will fail to compile.

This works because _Generic selection uses the type of a controlling expression ((Y) in this case) to select a rule to follow and insert code corresponding to the rule. In this case, we only provided a rule for __typeof__(X), thus if (X) is not a compatible type for (Y), there is no suitable rule to select and therefore cannot compile. To handle arrays, which have a controlling expression that will decay to a pointer, I added another _Generic that goes the other way ensuring they must both be compatible with one another rather than accepting one-way compatibility. And since--as far as I particularly cared--we only wanted to make sure it would fail to compile on a mismatch, rather than execute something particular upon a match, I gave the corresponding rule the task of doing nothing: (void)NULL

There is a corner case where this technique stumbles: _Generic does not handle Variably Modifiable types since it is handled at compile time. So if you attempt to do this with a Variable Length Array, it will fail to compile.

To handle your specific use-case for fixed-width unsigned types, we can modify the nested _Generic to handle that rather than handling the pecularities of an array:

#define TYPE_ASSERT(X,Y) _Generic ((Y), \
    __typeof__(X): _Generic ((Y), \
        uint8_t: (void)NULL, \
        uint16_t: (void)NULL, \
        uint32_t: (void)NULL, \
        uint64_t: (void)NULL \
   ) \
)

Example GCC error when passing non-compatible types:

main.c: In function 'main':
main.c:7:34: error: '_Generic' selector of type 'signed char' is not compatible with any association
    7 |         __typeof__(X): _Generic ((Y), \
      |                                  ^

It is worth mentioning that __typeof__, being a GCC extension, will not be a solution that is portable to all compilers. It does seem to work with Clang, though, so that's another major compiler supporting it.

Christian Gibbons
  • 4,272
  • 1
  • 16
  • 29
  • Good answer. Unfortunately it follows the lead of my shoddy example instead of my description. I have a macro where unsigned underflow and overflow is expected, so I need to verify that all inputs are either uint8_t, uint16_t, uint32_t, or uint64_t. They must be one of those and all the same. You've given me a good start though. Also, clang claims to be "gcc compatible", so they make it a point to try to support all gcc extensions and preprocessor directives possible, with identical syntax too. Clang documentation then documents any deviations or nuances & says to reference gcc documentation. – Gabriel Staples Mar 10 '20 at 07:57
  • @GabrielStaples I have updated with another example more specific to your use-case. – Christian Gibbons Mar 10 '20 at 08:16
1

What you want is doable in standard C11, no extensions or GCC required.

We'll build up to the final answer, so all can follow.


According to the C11 standard [6.7.10], static_assert-declaration: _Static_assert( constant-expression , string-literal ) is a Declaration. Thus if we are going to use a macro, we had best provide a scope for a declaration, to keep things tidy. Typically of the usual form:

#define MY_AMAZING_MACRO() do {_Static_assert(...some magic...);} while(0)

Next, so that our _Static_assert within the macro at least repeats via stdio the actual issue if the assert fails, well use familiar stringification setup:

#define STATIC_ASSERT_H(x)  _Static_assert(x, #x)
#define STATIC_ASSERT(x)    STATIC_ASSERT_H(x)

Next, we'll use C11's Generic selection feature to declare a macro that returns a constant 1 if the object is of the type we're looking for, and zero otherwise:

#define OBJ_IS_OF_TYPE(Type, Obj) _Generic(Obj, Type: 1, default: 0)

Next we''l make a macro to test if all four of your inputs are of the same type:

#define ALL_OBJS_ARE_OF_TYPE(Type, Obj_0, Obj_1, Obj_2, Obj_3)  \
    (OBJ_IS_OF_TYPE(Type, Obj_0) &&                             \
     OBJ_IS_OF_TYPE(Type, Obj_1) &&                             \
     OBJ_IS_OF_TYPE(Type, Obj_2) &&                             \
     OBJ_IS_OF_TYPE(Type, Obj_3))

Next, using the above, well make a macro to test if all four of your inputs are further one of the four types:

#define IS_ACCEPTABLE(Type_0, Type_1, Type_2, Type_3, Obj_0, Obj_1, Obj_2, Obj_3)   \
    (ALL_OBJS_ARE_OF_TYPE(Type_0, Obj_0, Obj_1, Obj_2, Obj_3) ||                    \
     ALL_OBJS_ARE_OF_TYPE(Type_1, Obj_0, Obj_1, Obj_2, Obj_3) ||                    \
     ALL_OBJS_ARE_OF_TYPE(Type_2, Obj_0, Obj_1, Obj_2, Obj_3) ||                    \
     ALL_OBJS_ARE_OF_TYPE(Type_3, Obj_0, Obj_1, Obj_2, Obj_3))

And FINALLY, putting it all together:

#define TEST_FUNC(a,b,c,d)                                              \
do                                                                      \
{                                                                       \
    STATIC_ASSERT(IS_ACCEPTABLE(uint8_t, uint16_t, uint32_t, uint64_t,  \
                                a,       b,        c,        d));       \
} while(0)

Of course, you could separate the above into more distinct, individual STATIC_ASSERTs, as you wish, if you want more verbose error output if any of the _Static_asserts fail.

Gunther Schulz
  • 575
  • 3
  • 18