你能写一个 printf 包装器来交错格式化代码和参数吗?
Could you write a printf wrapper that interleaves formatting code and arguments?
我喜欢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
结尾时,它才会查找另一个格式字符串。
另外,还有一些免责声明:
- 就像FAQ列表的
miniprintf
一样,我展示的版本是精简的和不完整的:它不做浮点数(%e
或%f
或%g
),它不做字段宽度或精度,它不返回适当的值,等等,等等。 - 严格来说,如果要使用空指针来终止这里的格式列表,就必须使用语法
(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;
}
- C++我的数学有什么问题,为什么我的代码不能正确循环
- 代码在main()中运行,但在函数中出现错误
- 在VS代码中交叉编译Windows与Linux上的MinGW的SDL程序
- 编译包含字符串的代码时遇到问题
- 为什么EclipseCDT代码格式化程序有时会在模板参数中引入空格
- Vscode 自动格式化代码在运行几次后停止工作
- 解锈化代码格式化程序删除C++单行中的空格
- 如何重新格式化我的代码以使用数组
- OPENCV解码灰色代码模式相机校准错误.如何格式化固有和外在结果
- 可以处理复杂列对齐的代码格式化程序
- 是否可以使用一行代码从 std::cin 中提取格式化输入
- WXSTRING:是否有使用类似Python3的占位符实现字符串格式化的C/C 代码
- 如何在 Emacs 中自动格式化代码,就像在 Netbeans IDE 中一样
- 防止CLion在右大括号上重新格式化代码
- 如何在视觉助手 X 中格式化代码
- C++代码自动格式化
- 如何为代码块禁用Eclipse CDT代码格式化程序
- C++将格式化的代码复制到单词(如视觉助手)
- 如何在emacs中自动格式化(而不仅仅是自动缩进)c++代码
- Visual studio C++插件,它按照一些规则格式化代码