简体   繁体   English

C++20 中的无宏日志和跟踪,具有概念和模板专业化

[英]Macro-free Logging and Tracing in C++20, with concepts and template specialization

I have been dabbling with new C++20 features such as modules and concepts .我一直在涉足新的 C++20 特性,例如模块概念 One of the inherent properties of the new modules is that they do not leak pre-processor definitions to consumers -- this is both a blessing and a curse because some behaviours, such as logging, in C++ have traditionally been implemented with #define macros so that they can be #define d to nothing in release builds.模块的固有属性之一是它们不会将预处理器定义泄露给使用者——这既是福也是祸,因为 C++ 中的某些行为(例如日志记录)传统上是使用#define宏实现的,因此他们可以在发布版本中#define d 为空。

My question is simple: how should one go about implementing logging without macros, today, assuming that one still wants to retain behaviours like having the compiler entirely remove logging calls, with no side effects, in release builds?我的问题很简单:今天应该如何在没有宏的情况实现日志记录,假设人们仍然希望在发布版本中保留诸如让编译器完全删除日志记录调用而没有副作用的行为?

My endeavour to achieve this exploits lambdas and C++20 concepts to drive template specialization.我努力实现这一目标利用lambdas和 C++20概念来驱动模板专业化。

#define NOOP /* no operation */

template <typename T>
concept printable = requires (const T & message) {
    std::cout << message;
};

template <typename F>
concept format_factory = std::regular_invocable<F>
&& std::convertible_to<std::invoke_result_t<F>, std::string_view>;

... ...

#ifdef _DEBUG
private:
    template<std::regular_invocable F>
    static inline const std::invoke_result_t<F> map(F f) {
        return f();
    }

    template<typename T>
    static inline constexpr const T& map(const T& value) {
        return value;
    }

public:
    template<printable T>
    static inline void trace(const T& message) {
        std::cout << message << std::endl;
    }

    template<typename... Args>
    static inline void trace(const std::string_view& format, Args&&... args) {
        std::cout << std::format(format, map(args)...) << std::endl;
    }

    template<format_factory F, typename... Args>
    static inline void trace(const F& format, Args&&... args) {
        std::cout << std::format(format(), map(args)...) << std::endl;
    }

#else
public:
    template<typename... Args>
    static inline constexpr void trace(const Args&... args) { NOOP; }
#endif

The idea is that...这个想法是...

  • any literals and values to be logged can be passed, normally, because, in release builds, the compiler will optimise out any copies or moves since they will not be accessed.通常可以传递要记录的任何文字和值,因为在发布版本中,编译器将优化任何副本或移动,因为它们不会被访问。
  • anything ' expensive ' to be logged can be passed as an accessor lambda which will not be invoked in release builds and, consequently, also be optimised out.任何要记录的“昂贵”都可以作为访问器lambda传递,它不会在发布版本中被调用,因此也会被优化掉。

For example, user-code might look like this:例如,用户代码可能如下所示:

Log::trace("format literal ({}, {})", []() { return "expensive value"; }, "cheap value");

I have tried this with Visual C++ 2022 (preview) and I can confirm that it does work as intended but is it a good idea?我已经用 Visual C++ 2022(预览版)尝试过这个,我可以确认它确实按预期工作,但这是个好主意吗? How could I make it better?我怎样才能让它变得更好?

Remember that this is done because I want to export this from a C++20 module and I can't do that with preprocessor macros as far as I understand.请记住,这样做是因为我想从 C++20模块中export它,而据我所知,我无法使用预处理器宏来做到这一点。

Modularized approach模块化方法

The entire approach with macro-definitions in headers in discouraged with modules.在模块中不鼓励在头文件中使用宏定义的整个方法。 Not that it's impossible, but those headers with logging configuration need to be parsed by the compiler for each source file that include them.并不是说这是不可能的,但是编译器需要为包含它们的每个源文件解析那些带有日志配置的头文件。

I suppose you could add the macro definitions at the command line, eg with gcc:我想您可以在命令行中添加宏定义,例如使用 gcc:

g++ [...] -DDEBUG_LOGGING

And with Visual C++ (from here ):并使用 Visual C++(来自此处):

[...] /p:DefineConstants="DEBUG_LOGGING"

Working with headers使用标题

"Remember that this is done because I want to export this from a C++20 module and I can't do that with preprocessor macros as far as I understand." “请记住,这样做是因为我想从 C++20 模块中导出它,而据我所知,我无法使用预处理器宏来做到这一点。”

--> If you want to export the logging functionality as a module, then the implementation cannot depend upon configurations of a header file. --> 如果要将日志功能export为模块,则实现不能依赖于头文件的配置。 In C++20 we have two ways of including headers: Inside the global module fragment and inside the module declaration:在 C++20 中,我们有两种包含头文件的方法:在全局模块片段内部和在模块声明内部:

module;
#define DEBUG_LOGGING      // or similar configuration
#include "logging.hpp"     // inside global module fragment

export module some_module;
// 1)
#define DEBUG_LOGGING
#include "logging.hpp"     // inside module declaration
// 2)
#define DEBUG_LOGGING
import logging;            // defined is ignored with module unit

Quoting from cppreference :引用cppreference

"#include should not be used in a module unit (outside the global module fragment), because all included declarations and definitions would be considered part of the module. Instead, headers can also be imported with an import declaration: " “#include 不应在模块单元中使用(在全局模块片段之外),因为所有包含的声明和定义都将被视为模块的一部分。相反,也可以使用导入声明来导入标头:“
And:和:
"Importing a header file will make accessible all its definitions and declarations. Preprocessor macros are also accessible (because import declarations are recognized by the preprocessor). However, contrary to #include, preprocessing macros defined in the translation unit will not affect the processing of the header file. This may be inconvenient in some cases (some header files uses preprocessing macros as a form of configuration), in which case the usage of global module fragment is needed. " “导入头文件将使其所有定义和声明都可以访问。预处理器宏也可以访问(因为导入声明被预处理器识别)。但是,与#include 相反,翻译单元中定义的预处理宏不会影响对头文件。这在某些情况下可能不方便(某些头文件使用预处理宏作为配置形式),在这种情况下需要使用全局模块片段。”

So the logging header may be included in the global module fragment, but then the compiler needs to process it several times.所以日志头可能包含在全局模块片段中,但是编译器需要多次处理它。 This is especially troublesome if that header is using a 3rd party library like spdlog or similar, in which case that header-inclusion becomes a recursive nightmare.如果该标头正在使用像 spdlog 或类似的第三方库,则这尤其麻烦,在这种情况下,标头包含成为递归噩梦。

Alternatively, the logging header may be included inside the module declaration, but then its definitions will leak outside that module, and the header file may not import any modules at all.或者,日志记录头可能包含在模块声明中,但其定义将泄漏到该模块之外,并且头文件可能根本不导入任何模块。 That last bit I tried with gcc and got the following error:最后一点我尝试使用 gcc 并得到以下错误:

log_helper.hpp:3:1: error: post-module-declaration imports must not be from header inclusion

Code sample代码示例

We should not depend on header file configuration to declare (and parse) our templated logging functions since that is a horribly slow process, so instead we can feed the configurations to the compiler statically (by instantiating a type).我们不应该依赖头文件配置来声明(和解析)我们的模板化日志函数,因为这是一个非常缓慢的过程,因此我们可以静态地将配置提供给编译器(通过实例化一个类型)。 The general idea is to compile the module once , and depend on consumers of the module to generate the appropriate function calls.一般的想法是编译一次模块,并依赖于模块的使用者来生成适当的函数调用。

Here is the code that you suggested:这是您建议的代码:

template<format_factory F, typename... Args>
static inline void trace(const F& format, Args&&... args) {
    std::cout << std::format(format(), map(args)...) << std::endl;
}

If we call it with trace ("arg={}", my_obj);如果我们用trace ("arg={}", my_obj);调用它trace ("arg={}", my_obj); then the compiler will generate code for that particular overload.然后编译器将为该特定重载生成代码。 However, if we then call it with a different number of arguments, eg trace ("arg_1={}, arg_2={}", my_obj1, my_obj2);但是,如果我们使用不同数量的参数调用它,例如trace ("arg_1={}, arg_2={}", my_obj1, my_obj2); , then the compiler will have to lex and parse the trace function again (including recursing through dependent templated types), which is a slow process. ,那么编译器将不得不再次对跟踪函数进行词法分析和解析(包括通过依赖模板类型进行递归),这是一个缓慢的过程。 With a module, we compile once and store that as an abstract syntax tree in binary format, so generating code after that will be much faster.对于模块,我们编译一次并将其存储为二进制格式的抽象语法树,因此之后生成代码会快得多。

This is my suggestion for a code sample.这是我对代码示例的建议。 I have omitted variadic template expansion to ease reading:我省略了可变参数模板扩展以方便阅读:

// logging.cpp
export module logging;

export class ILogger
{
public:
    virtual void Trace() = 0;
};

export enum class LogType
{
    null,
    default
};

class NullLogger : public ILogger
{
public:
    void Trace() override {};
};

export class Log
{
public:
    static void Configure(LogType lt) {
        switch(lt) {
            case LogType::null:
                mInstance = std::make_shared<NullLogger>();
                return;
            default:
                break;
        }
    }
    static void Trace() { mInstance->Trace(); }

private:
    std::shared_ptr<ILogger> mInstance;
};

I will have to take a look at optimized assembly (link-time optimizations), to check if the indirection for null logger is optimized away.我将不得不查看优化的程序集(链接时优化),以检查是否优化了空记录器的间接性。 Likely, some more experienced C++ folks can tell immediately.很可能,一些更有经验的 C++ 人可以立即分辨出来。

An idea for optimization would be to store store a function pointer for mInstance->Trace() to avoid vtable lookups.优化的一个想法是为mInstance->Trace()存储一个函数指针以避免 vtable 查找。 But again, the compiler might deduce that automatically.但同样,编译器可能会自动推断出这一点。 I have very limited experience with that particular subject.我对该特定主题的经验非常有限。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM