可以使用多个 _Generic 来创建字符串文字吗?

Can multiple _Generic be used to create a string literal?

有没有办法在同一表达式中多次使用 _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

因为显然泛型宏在预处理器中不算作字符串文字,所以字符串文字连接是不可能的。我玩弄了 "stringification" 宏,但没有成功。

好问题,你可以粘贴一个字符串传递另一个参数:

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

我要说答案是否定的。

首先,_Generic 关键字不是(也不可能是)预处理器指令。 generic-selection 是第 6.5.1 节中定义的主要表达式。给定输入

printf(CS(abc.a) "hello", abc.a);

预处理器的输出(由 -E 编译器选项生成)是:

printf(_Generic((abc.a), int: "%d", char: "%c", long: "%ld") "hello", abc.a);

请注意,无法进行字符串连接,因为 generic-selection 尚未计算。另请注意,预处理器无法评估,因为它需要了解 abc 是类型 ABC 的结构,它具有成员 a。预处理器做简单的文本替换,它不知道这些事情。

其次,第 5.1.1.2 节中定义的编译器阶段不允许在字符串连接之前评估 _Generic 关键字。规范中引用的相关阶段是

  1. Adjacent string literal tokens are concatenated.

  2. White-space characters separating tokens are no longer significant. Each preprocessing token is converted into a token. The resulting tokens are syntactically and semantically analyzed and translated as a translation unit.

_Generic 关键字必须在阶段 7 中进行评估,因为它需要的知识只有在对标记进行句法和语义分析后才可用,例如abc 是具有成员 a 的结构。因此,多个 _Generic 关键字不能利用字符串连接来生成单个字符串文字。

仅作记录,事实证明可以在编译时生成基于 _Generic 的字符串常量,方法是使用除预处理器可用的其他肮脏技巧。

我想出的解决方案太丑了,我几乎不敢post它,但我会这样做只是为了证明它可能。

不要这样写代码!

#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 的包装器,它准确地打印 2 个参数,无论类型如何,都带有正确的转换说明符。

它基于一个结构构建一个字符串,转换说明符字符串文字被传递到该结构。此类转换说明符的每个数组占位符都被故意声明为太小而无法容纳空终止符。

最后,这个结构被转储到一个 union 中,它可以将整个结构解释为一个字符串。当然,这是非常值得怀疑的做法(即使它没有违反严格的别名):如果有任何填充,那么程序将失败。