How to use static assert in C to check the types of parameters passed to a macro
Asked Answered
B

3

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]
Bleier answered 10/3, 2020 at 4:38 Comment(8)
C provides __typeof__ (GNU C provides typeof) that allow you to query for type. See Referring to a Type with typeofCryosurgery
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.Bleier
What I was thinking is like a if (sizeof(__typeof__ a) == sizeof(__typeof__ b)) would distinguish between the uintX_t flavors.Cryosurgery
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.Bleier
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.Cryosurgery
@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: gcc.gnu.org/onlinedocs/gcc/Alternate-Keywords.htmlCatalectic
@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.Cryosurgery
__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. :)Bleier
C
4

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.

Catalectic answered 10/3, 2020 at 5:27 Comment(2)
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.Bleier
@GabrielStaples I have updated with another example more specific to your use-case.Catalectic
I
2

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.

Inotropic answered 20/3, 2020 at 11:44 Comment(0)
M
0

This was inspired by Gunther's answer, but I wanted to provide a simpler solution without any extra macros. Here's how you can test the type of one variable:

_Static_assert(_Generic(EXPRESSION, TYPE1: 1, TYPE2: 1, default: 0), "wrong type");

Here's a concrete example that checks the type of the errno variable provided by errno.h:

_Static_assert(_Generic(errno, int: 1, default: 0), "wrong errno type");

I'm not exactly sure why you'd want to test that two variables have the same type. There might be a simpler way to solve your actual problem, but if I really had to make sure two variables have the same type, and I know the type of the first variable is valid and belongs to a small list, I would do:

_Static_assert(_Generic(VAR1, TYPE1: 1, TYPE2: 2) == _Generic(VAR2, TYPE1: 1, TYPE2: 2, default: 0), "wrong VAR2 type");

Of course you can do this for any number of variables to ensure they all have the same type.

I might make one macro to simplify all the code above:

#define MY_TYPE_ID(v) (_Generic(v, TYPE1: 1, TYPE2: 2, default: 0))
Meli answered 12/1 at 0:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.