创建具有验证和语法突出显示的自定义 printf

Creating a custom printf with validation and syntax highlighting

本文关键字:显示 自定义 printf 语法 验证 创建      更新时间:2023-10-16

我希望能够创建一个日志记录机制,它非常简单地将参数传递给printf,但我需要语法突出显示和输入验证。这是我到目前为止拥有的Log命名空间的大纲。

#pragma once
#include "pch.h"
namespace Log
{   
// Determines whether to create the console window or not.
// Turn off for before release.
extern bool CreateConsoleWindow;
// Initialisation.
extern bool Init();
// Writes a line to the console, appended with rn.
extern void WriteLine(const char* _Format, ...);
// Writes a line to the console, with a [Debug] prepend.
extern void DebugInfo(const char* _Format, ...);
// Writes a line to the console, with a [Server] prepend.
extern void ServerInfo(const char* _Format, ...);
// Destruction.
extern bool Dispose();
}

这是我对Log::WriteLine(_Format, ...)的实现:

// Writes a line to the console, appended with rn.
void WriteLine(const char* _Format, ...)
{
// Print the message.
char buffer[4096];
va_list args;
va_start(args, _Format);
auto rc = vsnprintf(buffer, sizeof(buffer), _Format, args);
va_end(args);
// Append the new line.
printf("rn");
}

但是当我这样做时,我从输入字符串中丢失了所有验证和语法突出显示,这对于初学者至关重要。例如:

auto hash = Crypto::GetHash(syntax[1].c_str()); // unsigned long, by way of multiple nested macros.
printf("Hash: %d", hash);

这将以石灰绿色显示%d,并且printfhash上会有一个警告,说它不会被显示,因为它期望%lu。我依靠VS来教我做错了什么,这样我就可以从错误中吸取教训。

如果我对我的函数做同样的事情:

auto hash = Crypto::GetHash(syntax[1].c_str()); // unsigned long, by way of multiple nested macros.
Log::WriteLine("Hash: %d", hash);

然后%d与字符串的其余部分是相同的棕色,并且没有验证错误。

这两件事是必不可少的。有没有办法使它正常工作?在经历了大约十年的 .NET 经验后,我是C++新手,学习曲线是巨大的。我想我应该从基础开始,只是一个简单的日志记录系统,这样我就可以随时以干净优雅的方式查看输出的内容。在 C# 中,这只需要几分钟就可以组合在一起,但在 C++ 中,现在是四个小时后,我在 Firefox 的三个窗口中打开了大约 40 个选项卡,我仍然用头撞着可能遇到的每一堵砖墙。

我尝试在命名空间中定义一个宏:

#define WriteLine(_Format, ...) printf(_Format, __VA_ARGS__)

但据说找不到printf。我试过用_Printf_format_string_装饰:

extern int _cdecl DebugInfo(_Printf_format_string_ const char* _Format, ...);

但这没有区别。我试过__attribute__((format(printf, 1, 2)))

extern void WriteLine(const char* _Format, ...) __attribute__((format(printf, 1, 2)));

但这抛出了大量关于期望{unexpected identifierexpected a declaration的错误,但没有说明在哪里,为什么,或如何。

对于这样一个基本要求,我对C++的要求是否过高?

现代代码编辑器和IDE(和编译器!(为printf系列函数提供了超出C++和C语言范围的额外支持。一个典型的例子是对格式字符串的扩展错误检查,这在 C 中根本无法完成,而且在 C++ 中只能带来很大的痛苦。有几种方法只需稍作修改即可实现您的目标。

<iostreams>解决方案

默认情况下,std::coutprintf同步,因此您可能实际上不需要直接处理printf。对于如何使用iostreams库框架实现各种结果,有很多解决方案,即使使用起来有点麻烦。它是类型安全的,因此不需要额外的 IDE 支持即可直接显示错误。

请注意,iostreams 因其复杂性和易用性而名声不佳。

类型安全可变参数模板

您可以按以下方式使用可变参数模板:


void add_format_specifier(std::ostream& out, int const&) {
out << "%d";
}
void add_format_specifier(std::ostream& out, char const*) {
out << "%s";
}
void add_format_specifier(std::ostream& out, hex const&) {
out << "%x";
}
template<typename... Args>
void WriteLine(Args&&... args) {
std::ostringstream format;
((void)0, ..., add_format_specifier(format, args));
printf(format.str().c_str(), args...);
}

此解决方案是类型安全的,并且经过编译器检查,同样不依赖于特殊大小写printf。就个人而言,我建议走这条路,因为它结合了最大的灵活性和简单的类型检查 - 并且取消了格式说明符;)。您可能希望决定使用printf作为低级原语是否真的是您想要在此处执行的操作,但它很容易交换为其他输出方法。

格式字符串的编译时检查

可以添加格式说明符的自定义编译时检查,然后可以static_assert。您需要的基本构建块是一个可变参数模板,该模板也采用格式说明符(基本上,您需要在可变参数函数中保留类型(,并按以下方式调用格式说明符:

template<std::size_t N>
consteval bool check_format(char const (&format)[N], std::size_t i) {
for(; i < N; ++i) {
if(format[i] == '%') {
if(i + 1 >= N) {
return false;
}
if(format[i + 1] == '%') {
++i; // skip literal '%'
} else {
return false; // no more specifiers expected
}
}
}
return true;
}
template<std::size_t N, typename T, typename... Args>
consteval bool check_format(char const (&format)[N], std::size_t i) {
for(; i < N; ++i) {
if(format[i] == '%') {
if(i + 1 >= N) {
return false; // unterminated format specifier
}
if(format[i + 1] == '%') {
++i; // skip literal '%'
} else {
if constexpr(std::is_same_v<T, int>) {
// quickly check if it is an acceptable integer format specifier
if(format[i + 1] != 'd' && format[i + 1] != 'x') {
return false;
} else {
return check_format<N, Args...>(format, i + 2);
}
} else {
return false; // unknown format specifier
}
}
}
}
return false;
}

有关更多上下文,请参阅此处。(注意:此示例依赖于 C++20consteval说明符,编译器可能不支持该说明符。使用constexpr可以实现类似的效果。

此解决方案将为您提供编译时错误,但不突出显示语法 - C++不会影响 IDE 绘制字符串的方式(但;)(。

宏解决方案

虽然您的宏#define WriteLine(_Format, ...) printf(_Format, __VA_ARGS__)实际上并不允许您在实现重命名printf之外的任何内容方面做那么多事情,但前提是用户代码还包括<cstdio>。如果您提供了包含该宏的标头,则可能希望仅在其中添加包含内容以方便使用。

只需要进行微小的改进,因此宏也适用于形式为WriteLine("abc")的调用:

#include <cstdio>
#define WriteLine(_Format, ...) printf(_Format __VA_OPT__(,) __VA_ARGS__)