你能写一个 printf 包装器来交错格式化代码和参数吗?

Could you write a printf wrapper that interleaves formatting code and arguments?

与 cout 相比,我喜欢 printf 提供的简洁格式说明符,例如 %1.3f,但不喜欢将要打印的所有变量都放在参数列表的末尾,这是每当您想添加要打印的新项目时,都容易出错。这就是我想做的。而不是

printf("\n\n%ix%i%c %1.2f\n",sw, sh, interlaced, refresh);

我愿意

printf("\n\n%i", sw, "x%i", sh, "%c ", interlaced, "%1.2f\n", refresh);

使用正确的包装函数似乎应该可以做到这一点,但编写可变参数函数对我来说是一种黑色艺术。或者有人已经写过这个?这似乎是一个显而易见的想法。

简单:

printf("\n\n");
printf("%i", sw);
printf("x%i", sh);
printf("%c ", interlaced);
printf("%1.2f", refresh);
printf("\n");

如果您使用的是 C++,您可以这样做:

template<typename ... Args, std::size_t ... N>
void myPrint_impl(std::tuple<Args...> tup, std::index_sequence<N...>)
{
    (std::printf(std::get<N * 2>(tup), std::get<N * 2 + 1>(tup)), ...);
}

template<typename ... Args>
void myPrint(Args ... args)
{
    return myPrint_impl(std::make_tuple(args...), std::make_index_sequence<sizeof...(args) / 2>{});
}

int main()
{
    myPrint("\n\n%i", 10, "x%i", 20, "%c ", 'o', "%1.2f\n", 1.234);
}

对于严格的 C 答案,可以,但人们不会,因为问题变成了争论什么时候结束?

普通格式说明符字符串将 numbertype 中的说明符匹配到后面的参数。当没有更多的说明符时,将忽略任何剩余的参数(如果有的话)。

但是如果你想交错,那么你必须有一个终止参数,比如NULL,以便发出信号end-of-arguments。您的交错 printf 将如下所示:

iprintf( "%d", my_int, "%s", "my string", "%f", my_float, NULL );

最后一个NULL参数是人们不喜欢的刺,很大程度上是因为它可能会被意外遗忘,导致UB代码!

因此,我不会提出上述语法的解决方案。

拯救邪恶的宏

是的,我们还没有完成。完全可以通过滥用 C 预处理器来创建您喜欢的语法!好消息是这也解决了 non-terminal 参数列表的 UB 问题。

坏消息是它需要很多 LOC 才能实现:具体来说,你必须声明你的魔术函数,然后用宏覆盖它的声明,这是一件邪恶的事情,因为宏没有对上下文的认识,并可能破坏完全不相关的东西。

(我会花几分钟时间为您设计和测试一些代码。)

编辑:嘿,实际上,我今晚真的不想重写 printf...(因为基本上这是需要的)。也就是说,这种奇怪的语法可以做,但没人愿意做。我没有。

嗯,邪恶的宏 + win!

的第一个解决方案

所以,这个问题好像一下子变得很热门了。 IDK 为什么 任何人都想这样做,但在这里你去:multi_printf 和家人:

// ABSOLUTELY NOT FULLY TESTED!
// NOT FOR PRODUCTION CODE!
// (Why would you do this anyway?)

#include <errno.h>
#include <iso646.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

int multi_vsnprintf(
  char       * buffer,
  size_t       size,
  const char * format,
  va_list      args )
{
  int result = 0;

  while (format and ((size-result > 0) or !buffer))
  {
    // This is the easy part: make the C standard library do all the work

    va_list args2;
    va_copy( args2, args );
    int n = vsnprintf( buffer ? buffer+result : NULL, buffer ? size-result : 0, format, args2 );
    va_end( args2 );

    // If anything went wrong we need to quit now to propagate the error result upward,
    // otherwise we just keep accumulating the number of characters output

    if (n < 0)
    {
      result = n;
      break;
    }
    result += n;

    // Now for the obnoxious part: skip args to find the next format string
    // This only understands standard C99 argument types and modifiers!
    // (POSIX throws some others in there too, methinks, etc. We ignore them.)

    for (const char * p = strchr( format, '%' );  p and *p;  p = strchr( p+1, '%' )) do
    {
      p = strpbrk( p+1, "%*csdioxXufFeEaAgGnp" );
      switch (*p)
      {
        case '%':
          break;

        case '*':
          va_arg( args, int );
          continue;

        case 'c': //if (p[-1] == 'l') va_arg( args, wint_t ); else va_arg( args, int       ); break;
                  va_arg( args, int );
                  break;
        case 's': if (p[-1] == 'l') va_arg( args, char * ); else va_arg( args, wchar_t * ); break;

        case 'd': case 'i': case 'o': case 'x': case 'X': case 'u':
          switch (p[-1])
          {
            case 'l': if (p[-2] == 'l') va_arg( args, long long );
                      else              va_arg( args, long      ); break;
            case 'j':                   va_arg( args, intmax_t  ); break;
            case 'z':                   va_arg( args, size_t    ); break;
            case 't':                   va_arg( args, ptrdiff_t ); break;
            default:                    va_arg( args, int       ); break;
          }
          break;

        case 'f': case 'F': case 'e': case 'E': case 'a': case 'A': case 'g': case 'G':
          if (p[-1] == 'L') va_arg( args, long double );
          else              va_arg( args,      double );  // 'l' and (none)
          break;

        case 'n': case 'p':
          va_arg( args, void * ); // all are pointer types
          break;

        default:
          p = NULL;
          result = -1;
          errno = EINVAL; // Invalid Argument
          break;
      }
    } while (0);

    format = va_arg( args, const char * );
  }

  return result;
}

int multi_vfprintf(
  FILE       * stream,
  const char * format,
  va_list      args )
{
  // In order to print to file we must first print to string
  
  // (1) get the length of the needed string
  va_list args2;
  va_copy( args2, args );
  int result = multi_vsnprintf( NULL, 0, format, args2 );
  va_end( args2 );
  
  // (2) print to the string then print the string to file
  if (result > 0)
  {
    char * s = (char *)malloc( result+1 );
    if (!s) result = -1;
    else if (multi_vsnprintf( s, result+1, format, args ) > 0)
    {
      result = fprintf( stream, "%s", s );
    }
    free( s );
  }
  return result;
}

#define multi_vprintf(format,args)         multi_vfprintf(  stdout,           format, args )
#define multi_vsprintf(buffer,format,args) multi_vsnprintf( buffer, SIZE_MAX, format, args )

int multi_snprintf(
  char       * buffer,
  size_t       size,
  const char * format,
  ... )
{
  va_list args;
  va_start( args, format );
  int result = multi_vsnprintf( buffer, size, format, args );
  va_end( args );
  return result;
}

int multi_fprintf(
  FILE       * stream,
  const char * format,
  ... )
{
  va_list args;
  va_start( args, format );
  int result = multi_vfprintf( stream, format, args );
  va_end( args );
  return result;
}

#define multi_printf(format,...)               multi_fprintf(  stdout,           format, __VA_ARGS__, NULL )
#define multi_fprintf(stream,format,...)       multi_fprintf(  stream,           format, __VA_ARGS__, NULL )
#define multi_sprintf(buffer,format,...)       multi_snprintf( buffer, SIZE_MAX, format, __VA_ARGS__, NULL )
#define multi_snprintf(buffer,size,format,...) multi_snprintf( buffer, size,     format, __VA_ARGS__, NULL )


int main()
{
  const char * s = "worlds alive";
  
  int n = multi_printf(
    //format string  //arguments
    "%s ",           "Hello",
    "%.5s%c",        s, '!',
    " -no-specs- ",  
    "%d",            17,
    " %f\n",         3.14159265
  );
  for (int k = 0; k < n; k++) printf( "-" );
  printf( "%d characters printed\n", n );
}

注意事项

这两种解决方案都有一个明显的缺点:C 编译器中有代码来检查 printf 系列函数是否具有正确的参数数量和类型以匹配格式参数。

以上两个版本都不能做到这一点。因此,例如,如果您 mis-match 参数的 类型 ,那么您就是 SOL。

你能写一个 printf 交错格式化代码和参数吗? 是的,有点。但不是通过围绕 printf 编写任何类型的“包装器”。要完全做到这一点(并且由于您所谓的可变参数函数的“黑色艺术”),我们必须从头开始编写我们自己的“多 printf”版本。

还有一个复杂的问题是 multi-printf 很难知道它何时完成。通常,printf 只读取一个格式字符串,然后对于格式字符串中的每个 %,它(通常)还需要一个参数。如果我们想要一个可以读取格式字符串和一些参数,然后是另一个格式字符串,然后是更多参数的版本,它如何知道何时完成?在处理一个格式字符串及其参数后,我们的 multi-printf 如何知道它是否应该寻找其他格式字符串?

记住,规则是可变参数函数必须能够从它获取的参数中判断它应该期望多少个参数(以及什么类型)。没有独立的方法可以知道这次实际传递了多少个参数。

所以我这里要实现的multi-printf和你想象的有点不一样。它接受任意数量的格式字符串,穿插参数,但是 最后一个格式字符串必须没有额外的参数(不包含 % 符号),或者它必须是 null指针。

此代码是对我的 C 课程笔记 question 15.4 of the C FAQ list. Please refer to question 15.4 and make sure you have a basic grasp of how the miniprintf function there works. (That question and others in section 15 should dispel at least some of the "black magic" concerning varargs functions; see also chapter 25 中的 miniprintf 函数的修改。)本质上,我正在使用 format-parsing 和 argument-interpolating 代码来自 miniprintf,并围绕它打一个 do/while 循环,以便它可以在每次调用时解析任意数量的附加格式字符串。

这是代码。它需要来自常见问题解答列表 question 20.10 的“助手”功能 baseconv

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

void
multiprintf(const char *fmt, ...)
{
    const char *p;
    int i;
    unsigned u;
    char *s;
    va_list argp;

    va_start(argp, fmt);

    do {
        int nperc = 0;

        for(p = fmt; *p != '[=10=]'; p++) {

            if(*p != '%') {
                putchar(*p);
                continue;
            }

            nperc++;

            switch(*++p) {
                case 'c':
                    i = va_arg(argp, int);
                    putchar(i);
                    break;

                case 'd':
                    i = va_arg(argp, int);
                    if(i < 0) {
                        i = -i;
                        putchar('-');
                    }
                    fputs(baseconv(i, 10), stdout);
                    break;

                case 'o':
                    u = va_arg(argp, unsigned int);
                    fputs(baseconv(u, 8), stdout);
                    break;

                case 's':
                    s = va_arg(argp, char *);
                    fputs(s, stdout);
                    break;

                case 'u':
                    u = va_arg(argp, unsigned int);
                    fputs(baseconv(u, 10), stdout);
                    break;

                case 'x':
                    u = va_arg(argp, unsigned int);
                    fputs(baseconv(u, 16), stdout);
                    break;
            }
        }

        if(nperc == 0) break;
    
        fmt = va_arg(argp, char *);

    } while(fmt != NULL);

    va_end(argp);
}

这里有一个调用它的测试程序:

int main()
{
    int sw = 12;
    int sh = 0;
    int interlaced = 'A';
    multiprintf("%d ", sw, "x%d ", sh, "%c\n", interlaced, NULL);
    char *s = "four";
    multiprintf("int: %d ", sw, "char: %c ", '3', "string: %s", s, "\n");
}

现在,虽然这段代码确实回答了您的问题,但我不得不说我非常怀疑它在实践中是否真的有用。 printf 已经很难正确调用了。许多程序员很难保持额外参数的数量(尤其是类型)正确,而这个新函数只会加剧这些困难。很难记住保持最后一个格式参数 %-free。 (定义一个额外的终止条件可能是有意义的,即只有当前一个格式字符串不以 \n 结尾时,它才会寻找另一个格式字符串。)

此外,还有一些免责声明:

  1. 与常见问题解答列表的 miniprintf 一样,我显示的版本是 stripped-down 并且不完整:它不会 floating-point(%e%f%g),它不处理字段宽度或精度,它不 return 正确的值,等等
  2. 严格来说,如果你想用一个空指针来终止这里的格式列表,你必须使用语法(char *)NULL;你不能使用纯 NULL。 (原因参见常见问题解答列表中的question 5.11。)

另一种可能的设想是,如果您可以将变量名直接嵌入到格式字符串中会怎样?回到你原来的例子,如果你可以把它写成

interpolatingprintf("%{sw:i} x%{sh:i} %{interlaced:c} %{refresh:1.2f}");

当然,问题是这永远无法与 interpolatingprintf 作为“常规”函数一起使用。这种事情需要编译器支持,解析格式字符串并挑选出变量名,以便它们可以被评估和传递。 (还有人想知道 interpolatingprintf("%{a+b:d}") 之类的事情是否可以或应该被允许。)


最后,为了完整起见,由于 Stack Overflow 答案应该是独立的,这里是来自常见问题解答列表 question 20.10:

baseconv 代码
char *baseconv(unsigned int num, int base)
{
    static char retbuf[33];
    char *p;

    if(base < 2 || base > 16)
        return NULL;

    p = &retbuf[sizeof(retbuf)-1];
    *p = '[=13=]';

    do {
        *--p = "0123456789abcdef"[num % base];
        num /= base;
    } while(num != 0);

    return p;
}