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

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

本文关键字:格式化 代码 参数 包装 printf 一个      更新时间:2024-09-28

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

printf("nn%ix%i%c %1.2fn",sw, sh, interlaced, refresh);

我想要

printf("nn%i", sw, "x%i", sh, "%c ", interlaced, "%1.2fn", refresh);

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

简单:

printf("nn");
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("nn%i", 10, "x%i", 20, "%c ", 'o', "%1.2fn", 1.234);
}

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

普通格式说明符字符串将数量类型说明符与后面的参数匹配。当没有更多的说明符时,任何剩余的参数(如果有的话)都会被简单地忽略。

但是,如果要交错,则必须具有终端参数(例如 NULL)才能发出参数结束信号。然后,交错的 printf 将如下所示:

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

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

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

邪恶的宏来拯救

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

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

(我需要几分钟来为您设计和测试一些代码。

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

呵呵,邪恶的宏+获胜的第一个解决方案!

所以,这个问题似乎突然变得非常流行。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,
" %fn",         3.14159265
);
for (int k = 0; k < n; k++) printf( "-" );
printf( "%d characters printedn", n );
}

警告

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

上述版本都无法做到这一点。因此,例如,如果您不匹配参数类型,那么您就是 SOL。

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

还有一个复杂因素,即多打印f很难知道何时完成。 通常,printf只读取一个格式字符串,然后对于格式字符串中的每个%,它(通常)期望再读取一个参数。 如果我们想要一个可以读取格式字符串和一些参数的版本,然后是另一个格式字符串,然后是更多的参数,它怎么知道什么时候完成? 在处理了一个格式字符串及其参数之后,我们的 multi-printf 如何知道它是否应该寻找额外的格式字符串?

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

因此,我将在这里实现的多打印f与您想象的略有不同。 它采用任意数量的格式字符串,穿插着参数,最后一个格式字符串必须没有其他参数(不包含%符号),或者必须是空指针。

此代码是对 C FAQ 列表问题 15.4 中miniprintf函数的修改。 请参阅问题 15.4,并确保您基本掌握了那里的miniprintf功能的工作原理。 (这个问题和第15节中的其他问题应该至少消除一些关于varargs函数的"黑魔法";另请参阅我的C课程笔记第25章。 从本质上讲,我从miniprintf中获取格式解析和参数内插代码,并围绕它进行do/while循环,以便它可以解析每次调用任意数量的附加格式字符串。

这是代码。 它需要一个"帮助"功能,baseconv,来自常见问题列表的问题 20.10。

#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 != ''; 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, "%cn", interlaced, NULL);
char *s = "four";
multiprintf("int: %d ", sw, "char: %c ", '3', "string: %s", s, "n");
}

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

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

  1. 就像FAQ列表的miniprintf一样,我展示的版本是精简的和不完整的:它不做浮点数(%e%f%g),它不做字段宽度或精度,它不返回适当的值,等等,等等。
  2. 严格来说,如果要使用空指针来终止这里的格式列表,就必须使用语法(char *)NULL;不能使用纯NULL。 (有关原因,请参阅常见问题解答列表中的问题 5.11。

另一种需要考虑的可能性是,如果可以直接在格式字符串中嵌入变量名称会怎样? 回到你原来的例子,如果你能把它写成类似的东西会怎样

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

当然,问题在于,这永远无法将interpolatingprintf作为"常规"功能一起使用。 这种事情需要编译器支持,以解析格式字符串并选择变量名称,以便可以评估和传递它们。 (人们也想知道像interpolatingprintf("%{a+b:d}")这样的事情是否可以或应该被允许。


最后,为了完整起见,由于堆栈溢出答案应该是独立的,以下是常见问题解答列表问题 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 = '';
do {
*--p = "0123456789abcdef"[num % base];
num /= base;
} while(num != 0);
return p;
}