可以使用多个 _Generic 来创建一个字符串字面量吗?

4

有没有一种方法可以在同一表达式中多次使用_Generic关键字以创建单个字符串文字?

我正在寻找的是一种方法,例如生成一个单个格式字符串,以传递给printf,其中所有的转换说明符都适应于正确的类型。

当我写这个答案时,我最终得到了一个相当丑陋的解决方案:

#include <stdio.h>

typedef struct {
   int a;
   char b;
   long c;
} ABC;

// printf conversion specifiers:
#define CS(x)   \
  _Generic((x), \
    int:  "%d", \
    char: "%c", \
    long: "%ld")


int main (void)
{
  ABC abc = {1, 'a', 2};

  printf(CS(abc.a), abc.a); printf(" ");
  printf(CS(abc.b), abc.b); printf(" ");
  printf(CS(abc.c), abc.c); printf(" ");

  return 0;
}

使用6个printf调用而不是1个,显然不是理想的选择。

问题在于我找不到一种将_Generic和字符串字面值连接起来的预处理器方法,例如:

printf(CS(abc.a) " ", abc.a); // doesnt work
printf(CS(abc.a) CS(abc.b), abc.a, abc.b); // doesnt work either

因为显然泛型宏在预处理器中不算作字符串字面量,所以字符串字面量的连接是不可能的。我尝试使用了“字符串化”宏,但没有成功。


1
是的,_Generic 是一个主表达式,并且仅在稍后的编译阶段进行评估,在字符串文字融合完成后才会执行。原因很简单,_Generic 需要类型信息,而这些信息在预处理器阶段不可用。 - Jens Gustedt
@JensGustedt 啊,那么这就根本不可能了。太糟糕了...我想我必须想出一些其他的解决方案,它不是基于预处理器,但可以在编译时进行评估。 - Lundin
在P99中,我已经以另一种方式解决了这个问题。类型通用的东西是在对...参数进行评估时完成的,并将它们转换为调用所有返回字符串的格式化函数。在格式化字符串本身中,您只需要放置%s即可。但是以其当前形式,这相当于hacky。gcc 4.9处理得非常糟糕,并将编译吹到有时超过2GiB的内存占用量。 - Jens Gustedt
3个回答

3
我认为答案是否定的。
首先,"_Generic"关键字不是(也不可能是)预处理器指令。根据第6.5.1节的定义,通用选择是一种主表达式。给定输入内容。
printf(CS(abc.a) "hello", abc.a);
预处理器生成的输出(由-E编译器选项生成)为:
printf(_Generic((abc.a), int: "%d", char: "%c", long: "%ld") "hello", abc.a);

请注意,无法进行字符串连接,因为通用选择没有被评估。还要注意,预处理器无法进行评估,因为它需要知道abc是类型为ABC的结构体,该结构体具有成员a。预处理器执行简单的文本替换,对这些事情一无所知。
其次,在第5.1.1.2节中定义的编译器阶段不允许在字符串连接之前评估_Generic关键字。从规范中引用相关阶段如下:
6.相邻的字符串文字标记被连接。 7.分隔标记的空格字符不再重要。每个预处理令牌都转换为令牌。生成的令牌作为翻译单元进行语法和语义分析和翻译。
由于_Generic关键字需要在令牌经过语法和语义分析之后才能获得可用的信息(例如,abc是一个具有成员a的结构体),因此必须在第7阶段对其进行评估。因此,多个_Generic关键字无法利用字符串连接生成单个字符串文字。

1
我会接受这个答案作为正确的答案,因为它包含了一个很好的解释,说明我的原始想法为什么行不通。然而,我还是忍不住为了好玩而发明了一个可怕的黑客攻击 :) - Lundin

2
很好的问题,您可以通过传递另一个参数来粘贴字符串:
#include <stdio.h>

typedef struct {
    int a;
    char b;
    long c;
} ABC;

// printf conversion specifiers:
#define CS2(x, y)   \
    _Generic((x), \
     int:  "%d" y, \
     char: "%c" y, \
     long: "%ld" y) 

int main (void)
{
    ABC abc = {1, 'a', 2};

    printf(CS2(abc.a, "Hello"), abc.a);
    return 0;
}

但是如果我需要3个或更多的字符串呢?那么它只会不断地堆积嵌套的宏调用。 - Lundin
实际上,如果与第二个_Generic语句结合使用,它将无法正常工作。考虑printf(CS2(abc.a, CS2(abc.b, "hello")), abc.a);。我遇到的问题仍然是一样的。 - Lundin

1
仅供参考,事实证明可以在编译时基于_Generic生成字符串常量,方法是使用预处理器不可用的其他技巧。
我想出的解决方案非常丑陋,以至于我几乎不敢发布它,但我将这样做只是为了证明它是可能的。
不要编写像这样的代码!
#include <stdio.h>

typedef struct {
   int a;
   char b;
   long c;
} ABC;

// printf conversion specifiers:
#define CS(x)   \
  _Generic((x), \
    int:  "%d", \
    char: "%c", \
    long: "%ld")

#pragma pack(push, 1)
#define print2(arg1,arg2)              \
{                                      \
  typedef struct                       \
  {                                    \
    char arr1 [sizeof(CS(arg1))-1];    \
    char space;                        \
    char arr2 [sizeof(CS(arg2))-1];    \
    char nl_nul[2];                    \
  } struct_t;                          \
                                       \
  typedef union                        \
  {                                    \
    struct_t struc;                    \
    char     arr [sizeof(struct_t)];   \
  } cs2_t;                             \
                                       \
  const cs2_t cs2 =                    \
  {                                    \
    .struc.arr1 = CS(arg1),            \
    .struc.space = ' ',                \
    .struc.arr2 = CS(arg2),            \
    .struc.nl_nul = "\n"               \
  };                                   \
                                       \
  printf(cs2.arr, arg1, arg2);         \
}
#pragma pack(pop)

int main (void)
{
  ABC abc = {1, 'a', 2};

  print2(abc.a, abc.b);
  print2(abc.a, abc.c);
  print2(abc.b, abc.c);

  return 0;
}

输出:

1 a
1 2
a 2

解释:

print2宏是printf的包装器,精确打印两个参数,无论类型如何,都使用正确的转换说明符。

它基于一个结构体构建字符串,将转换说明符字符串面值传递给该结构体。为此,故意声明了每个数组占位符的大小过小,无法容纳空终止符。

最后,将该结构体转储到union中,可以将整个结构体解释为单个字符串。当然,这种做法非常可疑(即使不违反严格别名规则):如果存在任何填充,则程序将失败。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接