【发布时间】:2021-10-21 06:04:16
【问题描述】:
我一直在尝试新的 C++20 功能,例如 modules 和 concepts。新 模块 的固有属性之一是它们不会将预处理器定义泄露给消费者——这既是福也是祸,因为 C++ 中的某些行为(例如日志记录)传统上具有已使用 #define 宏实现,因此它们可以在 release 构建中成为 #defined。
我的问题很简单:今天应该如何实现日志记录没有宏,假设人们仍然希望保留一些行为,比如让编译器完全删除日志记录调用,没有副作用,在发布版本?
我为实现这一目标所做的努力是利用 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
这个想法是......
- 通常可以传递要记录的任何文字和值,因为在 release 构建中,编译器将优化任何副本或移动,因为它们不会被访问。
- 任何要记录的 'expensive' 都可以作为访问器 lambda 传递,它不会在 release 构建中调用,因此,也被优化了。
例如,用户代码可能如下所示:
Log::trace("format literal ({}, {})", []() { return "expensive value"; }, "cheap value");
我已经在 Visual C++ 2022(预览版)中进行了尝试,我可以确认它确实按预期工作,但 这是个好主意吗?我怎样才能让它变得更好?
请记住,这样做是因为我想从 C++20 模块 中 export 这个,据我所知,我无法使用预处理器宏来做到这一点。
【问题讨论】:
-
有趣的想法,但不确定是否可行。您可以使用global module fragments 将宏与模块一起使用。
-
@HolyBlackCat:我知道我可以在全局模块片段中使用
#include标头,但这并不能帮助我导出 宏,是吗?我想我可以为我的日志记录代码制作一个老式的标头,但是我无法实现完全自我强加的、愚蠢的、几乎可以肯定是短命的 100% 基于模块的代码库的理想。不过,我对人们将如何处理这件事很感兴趣。 -
这个视频是关于测试的,但我想同样的原则也适用于日志记录:youtube.com/watch?v=irdgFyxOs_Y&t=3s
-
还有一件事,@HolyBlackCat。我的日志记录本质上是一个包装
spdlog的解耦层。如果它在模块Log.ixx中,那么我可以只使用spdlog标头,因为模块是作为预编译标头实现的,所以只要我不更改Log.ixx,我就不会遭受构建时间的困扰-封装大量标头库的惩罚。如果我创建了Log.h和#included,那么在所有其他模块中,在全局模块片段中,情况就不是这样了,我必须自己静态链接spdlog或设置一个PCH。我在一片绿地工作。 -
@HolyBlackCat:别担心!嘿......你促使我去阅读折叠表达式,这不是坏事。我已经写 C# 这么久了,最近,Rust 让我的 C++ 非常生锈和过时。 (这也是为什么我在我的未开发代码库中尝试做一些愚蠢的事情,比如只用模块编写。这是一个挑战,而我知道 headers+
.cpp倒退。)
标签: c++ logging c++20 c++-concepts c++-modules