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

2022-08-04 00:00:00 printf c++ visual-studio-2019

我希望能够创建一种日志记录机制,它非常简单地将参数传递给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 
.
    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 
.
    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("
");
    }

但当我这样做时,我会丢失输入字符串中的所有验证和语法突出显示,这对于初学者来说是必不可少的。例如:

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

这将以橙绿色显示%d,并且在printf中的hash上会有一个警告,说它不会显示,因为它需要%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__)

但IS表示找不到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(以及编译器!)提供对超出C++和C语言范围的printf函数族的额外支持。最好的例子是对格式字符串进行扩展的错误检查,这在C中根本不能完成,在C++中只需费很大的力气。只需稍加修改即可实现目标。

<iostreams>解决方案

By default, std::cout is synchronized with printf,所以您实际上可能不需要直接处理printf。有许多解决方案可以帮助您使用ioStreams库框架实现各种结果,尽管使用起来有点麻烦。它是类型安全的,因此不需要额外的IDE支持即可直接显示错误。

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

类型安全变量模板

您可以沿着lines of:

使用可变模板

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作为低级原语是否真的是您希望在此处执行的操作,但它可以很容易地交换为其他输出方法。

格式字符串的编译时检查

possible添加格式说明符的自定义编译时检查,然后您可以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;
}
有关更多上下文,请参阅here。(注意:本例依赖于C++20consteval说明符,您的编译器可能不支持该说明符。constexpr也可以达到类似的效果。)

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

宏解决方案

虽然您的宏#define WriteLine(_Format, ...) printf(_Format, __VA_ARGS__)实际上不允许您执行除重命名printf以外的所有操作,但它可以工作,前提是用户代码也包含<cstdio>。如果您为该宏提供了标题,您可能只希望在其中添加包含项以便于使用。

只需要做一点微小的改进,这样对于WriteLine("abc")形式的调用的宏also works:

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

相关文章