【问题标题】:Pass a void pointer to printf and have its type determined by the format string将 void 指针传递给 printf 并使其类型由格式字符串确定
【发布时间】:2020-07-14 10:43:20
【问题描述】:

我将简化我的情况,以便专注于实际问题。

假设我正在为printf 编写一个名为print_data 的封面函数。用户调用 print_data 并传入单个格式字符串,例如"%.1f" 以及代表数据的 void *

void print_data(const char *format, void *data);

我的工作是接受这些论点并以某种方式将它们传递给printf

我的问题是 printf 需要一个 value 而不是 pointer(字符串除外)。我无法确定用户传入的数据类型,除了自己手动读取格式字符串并相应地转换数据(例如,如果传递了“f”,则转换为float)。

“神奇”的解决方案是能够以某种方式取消对 void* 的引用,但这当然是不可能的。

很遗憾,我无法重构设计,因为问题并非如此简单,并且需要我收到一个void* 和一个格式字符串。

我的问题与printf by given pointer and format string. Issue with floats 几乎相同,只是看起来解决方案没有解决。

有什么想法可以做到这一点吗?

【问题讨论】:

  • 您必须解析格式字符串以确定它期望的类型,然后适当地转换和取消引用指针。
  • 我提到我可以做到这一点,但这个解决方案对我来说有点啰嗦。我知道这可能是唯一的方法 - 但理想情况下我正在寻找一个“神奇”的解决方案;)
  • that question 中的所有代码都是错误的。 I am looking for a "magic" solution你有_Generic吗?您可以在调用方访问data 的类型吗?为什么要设计这样的界面而不是使用v*printf?没有魔法,即使你这样做了,维护起来也比写一个解析器更难。
  • @KamilCuk 非常...
  • @GaryAllen:在这种情况下没有魔法。 C只是不是那么高级。你必须以艰难的方式去做。

标签: c


【解决方案1】:

您必须在函数中解析格式字符串并使用适当的值类型调用printf。要读取该值,您可以将 void 指针转换为由转换说明符确定的适当类型。

这是一个简单的例子:

#include <inttypes.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>

#define printf printf__
int printf(const char *, ...);

int print_data(const char *format, void *data) {
    const char *p = format;
    enum {
        FMT_none = 0,
        FMT_c   = 1,
        FMT_i   = 2,
        FMT_u   = 3,
        FMT_f   = 4,
        FMT_pc  = 5,
        FMT_pv  = 6,
        PREF_l  = (1 << 3),
        PREF_ll = (1 << 4),
        PREF_h  = (1 << 5),
        PREF_hh = (1 << 6),
        PREF_j  = (1 << 7),
        PREF_z  = (1 << 8),
        PREF_t  = (1 << 9),
        PREF_L  = (1 << 10),
    };
    int fmt = FMT_none;
    
    for (;;) {
        int cur_fmt = FMT_none;
        int prefix = 0;
        p = strchr(p, '%');
        if (!p)
            break;
        p++;  // skip the '%'
        // skip the flag characters, width and precision
        // note that invalid combinations will not be detected
        // such as %..d or %.+d
        p += strspn(p, " -#+0123456789.");
        // parse the length modifier if present
        switch (*p) {
        case 'l':
            p++;
            prefix = PREF_l;
            if (*p == 'l') {
                p++;
                prefix = PREF_ll;
            }
            break;
        case 'h':
            p++;
            prefix = PREF_h;
            if (*p == 'h') {
                p++;
                prefix = PREF_hh;
            }
            break;
        case 'j':
            p++;
            prefix = PREF_j;
            break;
        case 'z':
            p++;
            prefix = PREF_z;
            break;
        case 't':
            p++;
            prefix = PREF_t;
            break;
        case 'L':
            p++;
            prefix = PREF_L;
            break;
        }
        switch (*p++) {
        case '%':
            if (p[-2] != '%')
                return -1;
            continue;
        case 'c':
            cur_fmt = FMT_c;
            break;
        case 'd':
        case 'i':
            cur_fmt = FMT_i;
            break;
        case 'o':
        case 'u':
        case 'x': case 'X':
            cur_fmt = FMT_u;
            break;
        case 'a': case 'A':
        case 'e': case 'E':
        case 'f': case 'F':
        case 'g': case 'G':
            cur_fmt = FMT_f;
            break;
        case 's':
            cur_fmt = FMT_pc;
            break;
        case 'p':
            cur_fmt = FMT_pv;
            break;
        default:
            return -1;
        }
        if (fmt != FMT_none)
            return -1; // more than one format
        fmt = cur_fmt | prefix;
    }
    switch (fmt) {
    case FMT_none:
        return printf(format);
    case FMT_c:
        return printf(format, *(char *)data);
    case FMT_c | PREF_l:
        // the (wint_t) cast is redundant, omitted
        return printf(format, *(wchar_t *)data);
    case FMT_i:
        return printf(format, *(int *)data);
    case FMT_i | PREF_l:
        return printf(format, *(long *)data);
    case FMT_i | PREF_ll:
        return printf(format, *(long long *)data);
    case FMT_i | PREF_h:
        return printf(format, *(short *)data);
    case FMT_i | PREF_hh:
        return printf(format, *(signed char *)data);
    case FMT_i | PREF_j:
        return printf(format, *(intmax_t *)data);
    case FMT_i | PREF_z:
    case FMT_u | PREF_z:
        return printf(format, *(size_t *)data);
    case FMT_i | PREF_t:
    case FMT_u | PREF_t:
        return printf(format, *(ptrdiff_t *)data);
    case FMT_u:
        return printf(format, *(unsigned *)data);
    case FMT_u | PREF_l:
        return printf(format, *(unsigned long *)data);
    case FMT_u | PREF_ll:
        return printf(format, *(unsigned long long *)data);
    case FMT_u | PREF_h:
        return printf(format, *(unsigned short *)data);
    case FMT_u | PREF_hh:
        return printf(format, *(unsigned char *)data);
    case FMT_u | PREF_j:
        return printf(format, *(uintmax_t *)data);
    case FMT_f:
        // the cast (double) is redundant, but useful to prevent warnings
        return printf(format, (double)*(float *)data);
    case FMT_f | PREF_l:
        return printf(format, *(double *)data);
    case FMT_f | PREF_L:
        return printf(format, *(long double *)data);
    case FMT_pc:
        return printf(format, *(char **)data);
    case FMT_pc | PREF_l:
        return printf(format, *(wchar_t **)data);
    case FMT_pv:
        return printf(format, *(void **)data);
    default:
        return -1;
    }
}

注意事项:

  • 浮点格式的行为类似于scanf():如果data 指向float,则使用%f;如果它指向double,则使用%lfl 将被 printf 忽略,因为 float 值在传递给可变参数函数时会转换为 double

  • 这个函数需要一个指向char 的指针,格式为%c,尽管printf 需要一个int,它将被转换为unsigned char

  • 虽然printf 需要wint_t,但该函数需要一个指向wchar_t%lc 格式的指针。

  • C 标准允许的转换说明符%zd%tu,但相应的类型未由标准定义。传递具有其他符号的类型对于负值并不严格正确,但不太可能造成问题。

【讨论】:

  • 使用 union 这样的类型的问题是它的对齐大小可能比原始 void * 可以指向的某些对象更大。因此,与其使用pval,不如为每种情况强制转换和取消引用data,例如:case FMT_i:return printf(format, *(int *)data);.
  • 对于FMT_f 情况,额外转换为(double) 是多余的,因为float 值将通过默认参数提升 转换为double,但也许你为了可读性添加了演员表。
  • 您可以通过添加PREF_jPREF_tPREF_z 来解决这些缺点,增加要处理的case 标签的数量,但消除所有涉及size 的内容变量。
  • 关于case FMT_i | PREF_z:,你可能会觉得How to use “zd” specifier with printf()?很有趣。
  • @chux-ReinstateMonica:union 参考了我的答案中发布的初始代码,但它并不完全正确。 %zd 确实很有趣。 FMT_pc: | PREF_l 不受支持,但易于添加。
【解决方案2】:

有什么想法可以做到这一点吗?

解析格式 - 考虑到格式不仅有 说明符 "acdefginopsuxAEFGX%",而且有 修饰符 "hlhhlljztL",还有 许多 可能性.这很快就产生了几十个(如果不是 100 多个)有效的原始组合。

存在简化:说明符 "aefgAEFG" 都是 double 或更宽。 "uoxX"unsigned 或更宽。我认为有效的说明符/修饰符组合的数量归结为 (2(fp) +8(i) +8(u) +2(c) +2(s) +1(1 ))。

此处可能需要禁止使用说明符 "%n" - 没有多大意义。

格式解析示例:How to check that two format strings are compatible?

不清楚 OP 想用 widthprecision 做什么,如 "%*.*Lf" 中需要多个参数。


为 printf 编写封面函数的替代方法通过传递格式和void * 将不使用格式打印,只是将对象传递给宏并使用_Generic 来控制打印函数。在Formatted print without the need to specify type matching specifiers using _Generic 中探索。

【讨论】:

    【解决方案3】:

    正如链接问题的答案所指出的,您必须编写所有不同的演员表。在你的情况下它是可行的,因为只有一个 void*,所以转换集是有限的。

    【讨论】:

    • 我真的很想避免这样做:(但我明白是否必须这样做
    • 没有真正的选择。你需要打一通printf(const char*, int),打一通printf(const char*, float),等等。这些调用将分别包含*(int*)data*(float*)data 演员表。然后编译器会找出参数的去向,这取决于 ABI。
    【解决方案4】:

    我最终使用了https://github.com/mpaland/printf 上提供的无依赖 printf 库。

    下载后,基本上就像将所有接受“int”类型的 va_arg 实例更改为接受该类型的指针,然后取消引用该值一样简单。

    例如,

    ... = (int)va_arg(va, int)
    

    改为

    ... = (int)*va_arg(va, int*)
    

    我必须更改的唯一其他代码是关于双精度和浮点数的,我必须专门检查是否通过了“lf”或“f”。幸运的是,因为这个库写得很好而且很容易理解,我注意到在 'lf' 的情况下已经为我设置了一个标志(即 FLAGS_LONG)。

    然后我只需要检查是否设置了该标志,如果设置了,我将值解释为双精度值,否则我将其解释为浮点数。

    希望这有助于任何尝试实现类似功能的地方。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-06-03
      • 2011-11-13
      • 2013-06-25
      相关资源
      最近更新 更多