Rocketmagnet
Rocketmagnet

Reputation: 5870

Typechecking macro arguments in C

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

Answers (7)

Ionic
Ionic

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

Zachary Vander Klippe
Zachary Vander Klippe

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

user295691
user295691

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

Dipstick
Dipstick

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

Bart van Ingen Schenau
Bart van Ingen Schenau

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

ulidtko
ulidtko

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

Jens Gustedt
Jens Gustedt

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

Related Questions