【问题标题】:Creating a variadic function for generic structs and primitive types为泛型结构和原始类型创建可变参数函数
【发布时间】:2021-08-11 04:22:38
【问题描述】:

我想要一个接受泛型参数的函数,这可以通过可变参数函数实现,但我正在寻找一些建议/帮助。

基本上,该函数可以接受任何原始类型(int、double、char* 等)和某些本地结构/指向它的指针。我执行了以下操作,对基本类型和结构的 ENUM 值进行硬编码。

我想出了以下解决方案,但至少对于我在 ENUM 中使用的原始类型,它看起来并不整洁。我看到vfprintf 可以在参数为const char *format, ... 的可变参数函数中使用,但是如何使用这种方法处理结构和多个参数?

enum types {LONG, INT, FLOAT, DOUBLE, CHAR, STRUCT};

typedef struct
{
    int size;
    char data[64];
} Pkt;


void bar(int len, va_list ap)
{

    Pkt pkt = va_arg(ap, Pkt);
    printf ("[Bar] packet: %d, %s\n", pkt.size, pkt.data);
}

void foo(int len, ...)
{
    char *str;
    double val;
    int intVal;
    va_list ap;
    Pkt pkt;
     
    
    va_start(ap, len);
    bar(len, ap);
    va_end(ap);

    va_start(ap, len);
    
    for (int i=0; i<len; i++)
    {
        
        int type =  va_arg(ap, enum types);
        switch(type)
        {
            case CHAR:
            str = va_arg(ap, char*); 
            printf ("CHAR: %s\n", str);
            break;
            
            case DOUBLE:
            val = va_arg(ap, double);
            printf ("DOUBLE: %f\n", val);
            break;
            
            case INT:
            intVal = va_arg(ap, int);
            printf ("INT: %d\n", intVal);
            break;
            
            case STRUCT:
            pkt = va_arg(ap, Pkt);
            printf ("STRUCT: %d, %s\n", pkt.size, pkt.data);
            break;
            
            default:
            break;
        }
    }
    va_end(ap);
}

int main() 
{   
    Pkt pkt = {.size = 50, .data = {"Hello"}};
    
    char *str = "World";
    
    foo(2, STRUCT, pkt, CHAR, str);
    return 0;
}

【问题讨论】:

  • printf 系列在格式字符串中编码可变参数的数量和类型。你在单独的论点中做到这一点。这两种方法没有本质区别。
  • 除了在我的方法中,我不仅限于传递原始类型,不是吗?
  • 您可以发明自己的格式说明符而不是枚举。例如 %P 用于数据包结构。
  • 你有例子吗?你在用sprintf吗?仍在阅读它,但到目前为止找不到
  • 什么例子?如果你想要一个示例实现,那么只需阅读printf 系列的源代码。

标签: c variadic


【解决方案1】:

您使用enum 的方法可读性强且很好——坚持下去。如果您不需要传递其他信息,例如 printf 需要带有 widthflagsprecisionmodifiers 除了格式规范,你的方法完全没问题。

一个建议是使用sentinel value,这样您就不必计算参数。像这样:

 foo(STRUCT, pkt, CHAR, str, END);

可以(可能)编写一个不需要任何enums 和格式字符串的版本。通过使函数成为宏并在参数数量上重载宏,然后为每个带有 C11 _Generic 构造的参数添加一个“标签”,您可以区分不同的类型并执行不同的操作。您的代码使用enum 标记所有类型。下面的例子使用了函数指针:

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>

struct pkt {
    int size;
    char data[64];
};

// note - by passing a pointer to va_list, we are preserving
// the state of va_list and can use it in functions
int _foo_string(va_list *va) {
    printf("CHAR: %s\n", va_arg(*va, const char*));
    return 0;
}

int _foo_double(va_list *va) {
    printf("DOUBLE: %f\n", va_arg(*va, double));
    return 0;
}

int _foo_int(va_list *va) {
    printf("INT: %d\n", va_arg(*va, int));
    return 0;
}

int _foo_pkt(va_list *va) {
    const struct pkt pkt = va_arg(*va, struct pkt);
    printf("STRUCT: %d, %s\n", pkt.size, pkt.data);
    return 0;
}

// is passed a list of:
// <function_pointer, argument,>...., MARK_END
// call function pointer on each argument
int _foo(int unused, ...)
{
    va_list va;
    va_start(va, unused);
    while (1) {
        int (*fp)(va_list*) = va_arg(va, int(*)(va_list*));
        if (!fp) {
            break;
        }
        if (fp(&va) != 0) {
            return -1;
        }
    }
    va_end(va);
    return 0;
}

// user of your library can add additional functions here if he wants to    
#define _foo_FUNC_USER_ADDITIONAL_TYPES()

// overload the argument on _Generic to find out the function
#define _foo_FUNC(x) _Generic((x), \
    _foo_FUNC_USER_ADDITIONAL_TYPES() \
    char *: _foo_string, \
    const char *: _foo_string, \
    int: _foo_int, \
    struct pkt: _foo_pkt)
// for each argument, replace argument by "_Geeneric(arg), arg,"
#define _foo_ARGS_1(_1)  _foo_FUNC(_1), _1,
#define _foo_ARGS_2(_1, _2)  _foo_ARGS_1(_1) _foo_ARGS_1(_2)
#define _foo_ARGS_3(_1, _2, _3)  _foo_ARGS_2(_2) _foo_ARGS_1(_1)
#define _foo_ARGS_N(_3,_2,_1,N,...)  _foo_ARGS_##N
#define _foo_ARGS(...)  _foo_ARGS_N(__VA_ARGS__, 3, 2, 1)(__VA_ARGS__)
// add a trailing MARK_END to detect ending
#define foo(...)  _foo(0, _foo_ARGS(__VA_ARGS__) NULL)

int main() 
{   
    struct pkt pkt = {
        .size = 50,
        .data = "Hello",
    };
    const char *str = "World";
    foo(pkt, str); // just works
    return 0;
}

无论你采取什么方法,你必须列举所有可能的类型。我在我的YIO library 中探索了上述方法,您可能想浏览。如果采用这种方法,请考虑使用一些代码生成器,以便可以轻松生成宏重载的数量。您可以使用EVAL(EVAL(EVAL(...))) 宏方式重载宏,但根据我的经验,它会导致完全无法阅读的非常非常长的错误消息。

无论如何,C 并不是真正用于泛型编程。如果您想编写更有趣的通用函数,而不是诅咒和持续的痛苦,请考虑转向另一种支持模板的编程语言。 C++ 是最接近 C 且最突出的此类语言,但 Rust、D、Ada 和其他此类语言可能会引起人们的兴趣。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-12-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-03-11
    • 2015-08-27
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多