Reputation: 52549
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");
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
Upvotes: 2
Views: 4036
Reputation: 87386
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))
Upvotes: 0
Reputation: 625
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))
#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_assert
s fail.
Upvotes: 3
Reputation: 4370
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.
Upvotes: 4