【问题标题】:Typechecking macro arguments in CC 中的类型检查宏参数
【发布时间】:2011-06-10 09:48:00
【问题描述】:

是否可以对#define 宏的参数进行类型检查?例如:

typedef enum
{
    REG16_A,
    REG16_B,
    REG16_C
}REG16;

#define read_16(reg16)  read_register_16u(reg16); \
                        assert(typeof(reg16)==typeof(REG16));

上面的代码似乎不起作用。我做错了什么?

顺便说一句,我正在使用 gcc,我可以保证在这个项目中我将始终使用 gcc。代码不需要是可移植的。

【问题讨论】:

标签: c macros


【解决方案1】:

不,宏不能为您提供任何类型检查。但是,毕竟,为什么要宏?您可以编写一个 static inline 函数,该函数(可能)将被编译器内联 - 在这里您进行类型检查。

static inline void read_16(REG16 reg16) {
    read_register_16u(reg16);
}

【讨论】:

  • 没有。这也不做类型检查,甚至没有警告,(我知道,你可能和我一样惊讶)这就是我尝试另一种策略的原因。请参阅我的另一个问题:stackoverflow.com/questions/4669454/…
  • 进行错误检查,请参阅我对另一个问题的回答。
  • 它只在 C++ 中进行错误检查。这是一个关于 C 的问题。
【解决方案2】:

要继续 ulidtko 的想法,请使用 inline 函数并让它返回一些内容:

inline
bool isREG16(REG16 x) {
  return true;
}

有了这样的事情你可以做编译时断言:

typedef char testit[sizeof(isREG16(yourVariable))];

【讨论】:

    【解决方案3】:

    没有。 C 中的宏本质上是类型不安全的,尝试检查 C 中的类型充满了问题。

    首先,在没有可用类型信息的编译阶段通过文本替换来扩展宏。因此,编译器在进行宏扩展时完全不可能检查参数的类型。

    其次,当您尝试在扩展代码中执行检查时,例如问题中的 assert,您的检查将推迟到运行时,并且还会触发看似无害的构造,例如

    a = read_16(REG16_A);
    

    因为枚举数(REG16_AREG16_BREG16_C)属于int 类型,而不是REG16 类型。

    如果您想要类型安全,最好的办法是使用函数。如果您的编译器支持它,您可以声明函数inline,这样编译器就知道您希望尽可能避免函数调用开销。

    【讨论】:

    • 啊,但是宏可能会扩展为一些可以验证类型的代码。例如,可以使用 assert(sizeof(reg16)==4) 检查宏内参数的大小。为什么不用类型?
    • 无论如何,正如我在对 ulidtko 的评论中提到的,函数方法也不会产生警告或错误!疯了,我知道。这就是为什么我尝试使用宏来做到这一点。
    • 不幸的是,C 中的类型检查不够严格,无法检测到错误枚举的使用。您必须更改为对枚举具有更严格类型检查的语言(例如 C++),或者使用对枚举应用更严格规则的代码检查器。
    • 巴特是对的。枚举在技术上是 C 中的整数;他们没有自己独特的类型。
    【解决方案4】:

    gcc 支持 typeof

    例如取自 linux 内核的 typesafe min 宏

    #define min(x,y) ({ \
        typeof(x) _x = (x); \
        typeof(y) _y = (y); \
        (void) (&_x == &_y);        \
        _x < _y ? _x : _y; })
    

    但它不允许您比较两种类型。请注意,虽然指针比较会产生警告 - 您可以像这样进行类型检查(也来自 linux 内核)

    #define typecheck(type,x) \
    ({  type __dummy; \
        typeof(x) __dummy2; \
        (void)(&__dummy == &__dummy2); \
        1; \
    })
    

    大概你可以做类似的事情 - 即比较指向参数的指针。

    【讨论】:

      【解决方案5】:

      对于与整数相关的类型,C 中的类型检查有点松散;但是你可以利用大多数指针类型不兼容的事实来欺骗编译器。

      所以

      #define CHECK_TYPE(var,type) { __typeof(var) *__tmp; __tmp = (type *)NULL; }
      

      如果类型不同,这将给出警告,“来自不兼容的指针类型的赋值”。例如

      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
      }
      

      我确信有某种方法可以得到编译器错误。

      【讨论】:

      • 某处有错字吗?我不得不从 __tmp 中删除一个下划线。但我仍然收到“错误:分配中的左值无效”
      • 感谢您的更正——修复了两个问题。我做了 0 && 技巧,认为它可以防止空指针取消引用,但这完全没有必要。下次我只是复制并粘贴我的代码,而不是在我转录时尝试清理它......
      • GCC 警告变量已设置但未使用,添加 (void)__tmp;让这安静下来。 #define CHECK_TYPE(var,type) { __typeof(var) *__tmp; __tmp = (type *)NULL; (void)__tmp; }
      【解决方案6】:

      这是一个老问题,但我相信我有一个一般性的答案,根据Compiler Explorer 似乎可以在 MSVC、gcc 和 clang 上工作。

      #define CHECK_TYPE(type,var) { typedef void (*type_t)(type); type_t tmp = (type_t)0; if(0) tmp(var);}
      

      在每种情况下,如果类型不兼容,编译器都会生成有用的错误消息。这是因为它强加了用于函数参数的相同类型检查规则。

      它甚至可以在同一范围内多次使用而不会出现问题。这部分让我有些吃惊。 (我认为我必须利用“__LINE__”来获得这种行为)

      下面是我运行的完整测试,注释掉的行都会产生错误。

      #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");
      }
      

      在每种情况下,带有 -O1 及更高版本的编译器都会删除结果代码中宏的所有痕迹。

      使用 -O0 MSVC 将对函数的调用保留为零,但它在无条件跳转中被敲击,这意味着这不应该是一个问题。带有 -O0 的 gcc 和 clang 都删除了除了 tmp 变量的堆栈初始化为零之外的所有内容。

      【讨论】:

      • 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) 这是在宏中打开一个新块的结果。 typedef 和变量 tmp 将是这个内部块的本地。强制宏以分号结尾并创建相同效果(即创建一个新的本地块)的常见方法是使用#define CHECK_TYPE(type,var) do { ... } while (0),这可能在几乎所有情况下都更可取。
      【解决方案7】:

      Zachary Vander Klippeanswer 的基础上,我们甚至可以更进一步(以一种可移植的方式,尽管这不是必需的)并且另外确保传入类型的大小匹配使用通常用于在 C 中实现静态断言的“负数组长度”技巧传入的变量的大小(当然,在 C11 之前,它确实提供了新的 _Static_assert 关键字)。

      作为一个额外的好处,让我们加入一些const 兼容性。

      #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)
      

      将新的 typedef 引用为名为 tmp2 的变量(此外,还引用此变量)只是一种确保我们不会生成不必要的警告的方法,参见 -Wunused-local-typedefs 和喜欢。我们可以改用__attribute__ ((unused)),但那是不可移植的。

      这将解决原始示例中的整数提升“问题”。

      本着同样精神的例子,失败的陈述被注释掉:

      #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);
      }
      

      当然,即使这种方法也无法解决所有问题。例如,检查符号很困难,并且通常依赖于假设正在使用特定补码变体(例如,二进制补码)的技巧,因此不能通用。如果类型可以是结构,那就更不用说了。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-11-19
        • 2023-04-11
        • 1970-01-01
        • 1970-01-01
        • 2023-03-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多