Reputation: 5870
Is it possible to typecheck arguments to a #define macro? For example:
typedef enum
{
REG16_A,
REG16_B,
REG16_C
}REG16;
#define read_16(reg16) read_register_16u(reg16); \
assert(typeof(reg16)==typeof(REG16));
The above code doesn't seem to work. What am I doing wrong?
BTW, I am using gcc, and I can guarantee that I will always be using gcc in this project. The code does not need to be portable.
Upvotes: 10
Views: 20442
Reputation: 509
Building upon Zachary Vander Klippe's answer, we might even go a step further (in a portable way, even though that wasn't a requirement) and additionally make sure that the size of the passed-in type matches the size of the passed-in variable using the "negative array length" trick that was commonly used for implementing static assertions in C (prior to C11, of course, which does provide the new _Static_assert
keyword).
As an added benefit, let's throw in some const
compatibility.
#define CHECK_TYPE(type,var) \
do {\
typedef void (*type_t) (const type);\
type_t tmp = (type_t)(NULL);\
typedef char sizes[((sizeof (type) == sizeof (var)) * 2) - 1];\
if (0) {\
const sizes tmp2;\
(void) tmp2;\
tmp (var);\
}\
} while (0)
Referencing the new typedef as a variable named tmp2
(and, additionally, referencing this variable, too) is just a method to make sure that we don't generate more warnings than necessary, c.f., -Wunused-local-typedefs
and the like. We could have used __attribute__ ((unused))
instead, but that is non-portable.
This will work around the integer promotion "issue" in the original example.
Example in the same spirit, failing statements are commented out:
#include <stdio.h>
#include <stdlib.h>
#define CHECK_TYPE(type,var) \
do {\
typedef void (*type_t) (const type);\
type_t tmp = (type_t)(NULL);\
typedef char sizes[((sizeof (type) == sizeof (var)) * 2) - 1];\
if (0) {\
const sizes tmp2;\
(void) tmp2;\
tmp (var);\
}\
} while (0)
int main (int argc, char **argv) {
long long int ll;
char c;
//CHECK_TYPE(char, ll);
//CHECK_TYPE(long long int, c);
printf("hello world\n");
return EXIT_SUCCESS);
}
Naturally, even that approach isn't able to catch all issues. For instance, checking signedness is difficult and often relies on tricks assuming that a specific complement variant (e.g., two's complement) is being used, so cannot be done generically. Even less so if the type can be a structure.
Upvotes: 2
Reputation: 108
This is an old question, But I believe I have a general answer that according to Compiler Explorer apears to work on MSVC, gcc and clang.
#define CHECK_TYPE(type,var) { typedef void (*type_t)(type); type_t tmp = (type_t)0; if(0) tmp(var);}
In each case the compiler generates a useful error message if the type is incompatible. This is because it imposes the same type checking rules used for function parameters.
It can even be used multiple times within the same scope without issue. This part surprises me somewhat. (I thought I would have to utilize "__LINE__" to get this behavior)
Below is the complete test I ran, commented out lines all generate errors.
#include <stdio.h>
#define CHECK_TYPE(type,var) { typedef void (*type_t)(type); type_t tmp = (type_t)0; if(0) tmp(var);}
typedef struct test_struct
{
char data;
} test_t;
typedef struct test2_struct
{
char data;
} test2_t;
typedef enum states
{
STATE0,
STATE1
} states_t;
int main(int argc, char ** argv)
{
test_t * var = NULL;
int i;
states_t s;
float f;
CHECK_TYPE(void *, var); //will pass for any pointer type
CHECK_TYPE(test_t *, var);
//CHECK_TYPE(int, var);
//CHECK_TYPE(int *, var);
//CHECK_TYPE(test2_t, var);
//CHECK_TYPE(test2_t *, var);
//CHECK_TYPE(states_t, var);
CHECK_TYPE(int, i);
//CHECK_TYPE(void *, i);
CHECK_TYPE(int, s); //int can be implicitly used instead of enum
//CHECK_TYPE(void *, s);
CHECK_TYPE(float, s); //MSVC warning only, gcc and clang allow promotion
//CHECK_TYPE(float *, s);
CHECK_TYPE(float, f);
//CHECK_TYPE(states_t, f);
printf("hello world\r\n");
}
In each case the compiler with -O1 and above did remove all traces of the macro in the resulting code.
With -O0 MSVC left the call to the function at zero in place, but it was rapped in an unconditional jump which means this shouldn't be a concern. gcc and clang with -O0 both remove everything except for the stack initialization of the tmp variable to zero.
Upvotes: 7
Reputation: 7248
The typechecking in C is a bit loose for integer-related types; but you can trick the compiler by using the fact that most pointer types are incompatible.
So
#define CHECK_TYPE(var,type) { __typeof(var) *__tmp; __tmp = (type *)NULL; }
This will give a warning, "assignment from incompatible pointer type" if the types aren't the same. For example
typedef enum { A1,B1,C1 } my_enum_t;
int main (int argc, char *argv) {
my_enum_t x;
int y;
CHECK_TYPE(x,my_enum_t); // passes silently
CHECK_TYPE(y,my_enum_t); // assignment from incompatible pointer type
}
I'm sure that there's some way to get a compiler error for this.
Upvotes: 9
Reputation: 10119
gcc supports typeof
e.g. a typesafe min macro taken from the linux kernel
#define min(x,y) ({ \
typeof(x) _x = (x); \
typeof(y) _y = (y); \
(void) (&_x == &_y); \
_x < _y ? _x : _y; })
but it doesn't allow you to compare two types. Note though the pointer comparison which Will generate a warning - you can do a typecheck like this (also from the linux kernel)
#define typecheck(type,x) \
({ type __dummy; \
typeof(x) __dummy2; \
(void)(&__dummy == &__dummy2); \
1; \
})
Presumably you could do something similar - i.e. compare pointers to the arguments.
Upvotes: 10
Reputation: 15768
No. Macros in C are inherently type-unsafe and trying to check for types in C is fraught with problems.
First, macros are expanded by textual substitution in a phase of compilation where no type information is available. For that reason, it is utterly impossible for the compiler to check the type of the arguments when it does macro expansion.
Secondly, when you try to perform the check in the expanded code, like the assert
in the question, your check is deferred to runtime and will also trigger on seemingly harmless constructs like
a = read_16(REG16_A);
because the enumerators (REG16_A
, REG16_B
and REG16_C
) are of type int
and not of type REG16
.
If you want type safety, your best bet is to use a function. If your compiler supports it, you can declare the function inline
, so the compiler knows you want to avoid the function-call overhead wherever possible.
Upvotes: 1
Reputation: 15560
No, macros can't provide you any typechecking. But, after all, why macro? You can write a static
inline
function which (probably) will be inlined by the compiler - and here you will have type checking.
static inline void read_16(REG16 reg16) {
read_register_16u(reg16);
}
Upvotes: 3
Reputation: 78903
To continue the idea of ulidtko, take an inline
function and have it return something:
inline
bool isREG16(REG16 x) {
return true;
}
With such as thing you can do compile time assertions:
typedef char testit[sizeof(isREG16(yourVariable))];
Upvotes: 1