【问题标题】:Creating a custom printf with validation and syntax highlighting创建带有验证和语法高亮的自定义 printf
【发布时间】:2020-01-19 06:16:01
【问题描述】:

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

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

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__)

但是说它找不到 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++ 的要求是否过高?

【问题讨论】:

    标签: c++ printf visual-studio-2019


    【解决方案1】:

    现代代码编辑器和 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++20 的 consteval 说明符,你的编译器可能不支持它。使用constexpr 可以达到类似的效果。)

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

    宏观解决方案

    虽然您的宏 #define WriteLine(_Format, ...) printf(_Format, __VA_ARGS__) 并不能真正让您执行除重命名 printf 之外的任何操作,但只要用户代码还包含 &lt;cstdio&gt;,它就可以工作。如果您为该宏提供标题,您可能希望在其中添加包含以方便使用。

    只需要进行微小的改进,以便宏 also works 用于WriteLine("abc") 形式的调用:

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

    【讨论】:

    • 非常感谢您的清晰解释,并花时间详细解释这一点。我已经尝试了你建议的每一种方法,但这里没有任何东西能达到我需要的效果。如果它看起来和行为都不像printf,那对我来说毫无用处。我不敢相信在 C++ 中做这样的事情是不可能的,这在 C# 中完全是自动的。 Log.WriteLine($"Hello {"World"} {0}");。我需要蓝色的波浪线来告诉我出了什么问题,否则我每次构建时都会把头发扯掉。定义仍然根本不起作用:namespace "Log" has no member "printf"
    • 注意:标记为正确答案,因为这个答案足以让大多数人回顾这个问题。虽然它没有解决我的问题,但似乎我的具体要求无法实现,这就是这个答案所推断的。斗争还在继续……
    • 为了完成,我决定放弃文体优雅,只使用printf 进行所有日志记录。我删除了命名空间,因为它的唯一目的是创建一种优雅的美化方式将文本输出到控制台。如果这不可能,那么命名空间就是多余的。我使用了这篇文章中的宏解决方案,通过隐藏全小写的名称来赋予代码优雅的外观,但除此之外没有。
    • @Apache 宏只执行基本的字符串替换,因此与namespace 进行了奇怪的交互。我不确定您实际上在编译什么 (the example works fine),但我想它可能与 this 类似。
    • 原来 VC++(还是?)does not support 是标准的 __VA_OPT__,但它会默默地忽略尾随的逗号,这使得很难提供可移植的解决方案
    猜你喜欢
    • 2016-12-30
    • 2011-09-03
    • 1970-01-01
    • 2017-09-13
    • 2020-07-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多