代码之家  ›  专栏  ›  技术社区  ›  Koldar

用函数指针参数解决“从不兼容指针类型传递函数参数”的标准方法

  •  1
  • Koldar  · 技术社区  · 6 年前

    我有一个 list 实现前向列表的模块,如下所示(最简单的工作示例):

    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>
    #include <stdbool.h>
    
    struct list_cell {
        void* payload;
        struct list_cell* next;
    };
    
    struct list {
        struct list_cell* head;
        int length;
    };
    
    typedef bool(*matcher_t)(const void* data);
    
    void* findElementInList(struct list* l, matcher_t matcher) {
        struct list_cell* tmp = l->head;
        while (tmp != NULL) {
            if (matcher(tmp->payload)) {
            return tmp->payload;
            }
            tmp = tmp->next;
        }
        return NULL;
    } 
    
    struct person {
        char* name;
        char* surname;
    };
    
    
    static char* constName = "Thomas";
    bool matchByName(const struct person* p) {
        printf("comparing %s with %s\n", p->name, constName);
        return strcmp(p->name, constName) == 0;
    }
    
    int main() {
        //initializations (made by hand to have a MWE)
        struct person globalThomas = {"Thomas", "Lot"};
    
        struct list* l = (struct list*) malloc(sizeof(struct list));
        l->head = (struct list_cell*) malloc(sizeof(struct list_cell));
        l->head->payload = &globalThomas;
        l->head->next = NULL;
        l->length = 1;
    
        void* el = findElementInList(l, matchByName);
        if (el != NULL) {
        printf("found Thomas!\n");
        } else {
        printf("Thomas not found!\n");
        }
        //deallocation
        free(l->head);
        free(l);
    }
    

    使用gcc编译此文件将生成以下警告:

    list.c:57:34: warning: passing argument 2 of ‘findElementInList’ from incompatible pointer type [-Wincompatible-pointer-types]
      void* el = findElementInList(l, matchByName);
                                      ^
    list.c:18:7: note: expected ‘matcher_t {aka _Bool (*)(const void *)}’ but argument is of type ‘_Bool (*)(const struct person *)’
     void* findElementInList(struct list* l, matcher_t matcher) {
    

    发生这种情况是因为 matcher_t 使用定义其参数 void* 但我们注射 struct person* 。我想解决这个警告,但我不确定最好的解决方法。清楚(如中所述 in this SO answer )我可以通过改变 matchByName 签名至 matchByName(const void* p) 通过铸造 无效* 指针指向 结构人* 。但我觉得这使函数成为函数的目的:仅通过查看标题,我无法确定函数的输入是什么。离开 结构人* 而是让它变得清晰。

    所以我的问题是: 解决此警告的最佳方法是什么?你通常怎么解决这个问题?除了换衣服,还有别的办法吗 matchByName 签名

    谢谢你的回复

    PS:这里的目标是添加 -Werror 编译中的标志,以使代码更加健壮。

    3 回复  |  直到 6 年前
        1
  •  1
  •   zwol    6 年前

    在C中,这是您所能做的最好的:

    bool matchByName(const void *px)
    {
        const struct person *p = px;
    
        printf("comparing %s with %s\n", p->name, constName);
        return strcmp(p->name, constName) == 0;
    }
    

    您必须使函数签名符合其调用者的期望,并且其调用者是伪泛型的,因此它必须通过 const void * 而不是具体的类型。这是无法避免的。但是,通过将非类型化参数指定给一个具有正确具体类型的变量作为函数中的第一条语句,您可以让阅读代码的人明白这一点,这是您所能做的最好的事情。

    顺便说一句,你真的应该摆脱它 constName 全局变量,通过 findElementInList 接受额外的 常量无效* 未受干扰地传递给匹配器的参数:

    bool matchByName(const void *px, const void *cx)
    {
        const struct person *p = px;
        const char *nameToMatch = cx;
    
        printf("comparing %s with %s\n", p->name, constName);
        return strcmp(p->name, constName) == 0;
    }
    
    void *findElementInList(struct list *l, matcher_t matcher,
                            const void *matcher_args)
    {
         struct list_cell *tmp = l->head;
         while (tmp) {
             if (matcher(tmp->payload, matcher_args)) {
                 return tmp->payload;
             }
             tmp = tmp->next;
         }
         return 0;
     }
    

    还请注意我对您的代码所做的文体更正:

    • 出于历史原因,在C语言中,首选的样式是将函数定义的大括号放在自己的行中,即使所有其他大括号都被语句头“抱住”。在某些代码中,您还会在其自身的行上看到返回类型,我认为这在返回类型上经常有很多限定符时是有意义的,但除非您打算这样做,否则不要这样做 一贯地 贯穿整个代码库。

    • 这个 * 在指针声明中绑定到 正当 ,而不是它左边的东西,所以它应该总是在左边写一个空格,在右边没有空格。任何人说不这样的话都是错误的。

    • 指针与NULL/0的显式比较风格不好;写就行了 if (ptr) (或者,在这种情况下, while (ptr) )。我个人认为NULL本身是一种糟糕的样式,应该改为写0,但理智的人可能不同意这一点。

        2
  •  0
  •   Lundin Makoto    6 年前

    函数指针只有在其类型相同时才兼容。严格来说,从一种类型到另一种类型的广泛转换是未定义的行为(尽管某些情况下可能在许多系统上作为非标准扩展)。

    因此,您要么在所有情况下都需要使用相同的类型,要么编写如下包装函数:

    inline bool matchByName (const void* p)
    {
      return matchByNamePerson (p);
    }
    

    也就是说,使用void指针的泛型编程是老式的C语言,非常危险。现在,您最好编写完全类型安全的代码:

    _Generic matchByName ((p), \
                          const struct person*: matchByNamePerson, \
                          const struct thing*:  matchByNameThing)(p)
    
        3
  •  0
  •   Koldar    6 年前

    可以同时保留警告缺失和代码可读性的解决方案可能是引入以下宏:

    #define PDOC(...) void*
    
    //function declaration
    bool matchByName(const PDOC(struct person*) p);
    
    //function definition
    bool matchByName(const PDOC(struct person*) _p) {
      const struct person* p = (const struct person*) _p;
      //insert awesome code here
    }
    

    通过这种方式,您不会生成警告,但可以保持签名的可读性(尽管它需要一些额外的类型)。