10

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.

Rocketmagnet
  • 5,656
  • 8
  • 36
  • 47
  • as far as I know C doesn't store information about type, so you can't get it runtime. – Mariy Jan 17 '11 at 11:48
  • Related: 1) [Use static_assert to check types passed to macro (my own really thorough answer in C++)](https://stackoverflow.com/questions/4021981/use-static-assert-to-check-types-passed-to-macro/60769143#60769143) and 2) [Static assert in C](https://stackoverflow.com/questions/3385515/static-assert-in-c/54993033#54993033) and 3) [How to use static assert in C to check the types of parameters passed to a macro](https://stackoverflow.com/questions/60611626/how-to-use-static-assert-in-c-to-check-the-types-of-parameters-passed-to-a-macro) – Gabriel Staples Mar 29 '20 at 08:50

7 Answers7

9

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.

Dipstick
  • 9,854
  • 2
  • 30
  • 30
9

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.

user295691
  • 7,108
  • 1
  • 26
  • 35
  • 1
    Is there a typo somewhere? I had to remove one underscore from __tmp. But I still get "error: invalid lvalue in assignment" – Rocketmagnet Nov 16 '11 at 17:15
  • Thanks for the correction -- made the two fixes. I did the 0 && trick thinking it would prevent a null pointer dereference, but that is totally unnecessary. Next time I'll just copy and paste my code instead of trying to clean it up as I transcribe it... – user295691 Nov 16 '11 at 20:38
  • GCC warns a variable is set but not used, add (void)__tmp; to quiet this. `#define CHECK_TYPE(var,type) { __typeof(var) *__tmp; __tmp = (type *)NULL; (void)__tmp; }` – ideasman42 Aug 25 '12 at 19:27
7

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.

  • `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)` This is a consequence of opening a new block within the macro. The `typedef` and variable `tmp` will be local to this inner block. A common way to enforce macros end on a semicolon, and creating the same effect (i.e., creating a new local block), would be to use `#define CHECK_TYPE(type,var) do { ... } while (0)`, which is probably preferable in almost all cases. – Ionic Mar 19 '21 at 03:44
3

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);
}
ulidtko
  • 14,740
  • 10
  • 56
  • 88
  • No. this does not do type checking either, not even a warning, (I know, you're probably as surprised as I am) that's why I'm trying another tactic. See my other question: http://stackoverflow.com/questions/4669454/how-to-make-gcc-warn-about-passing-wrong-enum-to-a-function – Rocketmagnet Jan 17 '11 at 12:43
  • This *does* error checking, see my answer to the other question. – ulidtko Jan 17 '11 at 12:54
  • 1
    It only does error checking in C++. This is a question about C. – Rocketmagnet Jan 17 '11 at 13:04
2

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.

Ionic
  • 499
  • 4
  • 18
1

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))];
Jens Gustedt
  • 76,821
  • 6
  • 102
  • 177
1

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.

Bart van Ingen Schenau
  • 15,488
  • 4
  • 32
  • 41
  • Aah, but the macro might expand to some code which can then verify the type. For example, it would be possible to check the size of the argument inside a macro using assert(sizeof(reg16)==4). Why not with type? – Rocketmagnet Jan 17 '11 at 12:55
  • Anyway, as I mentioned in my comment to ulidtko, the function method doesn't generate a warning or an error either! Crazy, I know. Which is why I was trying to do it using a macro. – Rocketmagnet Jan 17 '11 at 12:56
  • 4
    Unfortunately, typechecking in C is not strict enough to detect the use of the wrong enumeration. You must either change to a language that has stricter typechecking for enums (for example, C++), or use a code-checker that applies stricter rules to enums. – Bart van Ingen Schenau Jan 17 '11 at 13:45
  • Bart is right. Enums are technically integers in C; they don't have their own distinct type. – Vercingatorix Jun 14 '17 at 22:17