![](/img/trans.png)
[英]C++20 Concepts : Which template specialization gets chosen when the template argument qualifies for multiple concepts?
[英]Macro-free Logging and Tracing in C++20, with concepts and template specialization
我一直在涉足新的 C++20 特性,例如模塊和概念。 新模塊的固有屬性之一是它們不會將預處理器定義泄露給使用者——這既是福也是禍,因為 C++ 中的某些行為(例如日志記錄)傳統上是使用#define
宏實現的,因此他們可以在發布版本中#define
d 為空。
我的問題很簡單:今天應該如何在沒有宏的情況下實現日志記錄,假設人們仍然希望在發布版本中保留諸如讓編譯器完全刪除日志記錄調用而沒有副作用的行為?
我努力實現這一目標利用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
這個想法是...
例如,用戶代碼可能如下所示:
Log::trace("format literal ({}, {})", []() { return "expensive value"; }, "cheap value");
我已經用 Visual C++ 2022(預覽版)嘗試過這個,我可以確認它確實按預期工作,但這是個好主意嗎? 我怎樣才能讓它變得更好?
請記住,這樣做是因為我想從 C++20模塊中export
它,而據我所知,我無法使用預處理器宏來做到這一點。
在模塊中不鼓勵在頭文件中使用宏定義的整個方法。 並不是說這是不可能的,但是編譯器需要為包含它們的每個源文件解析那些帶有日志配置的頭文件。
我想您可以在命令行中添加宏定義,例如使用 gcc:
g++ [...] -DDEBUG_LOGGING
並使用 Visual C++(來自此處):
[...] /p:DefineConstants="DEBUG_LOGGING"
--> 如果要將日志功能export
為模塊,則實現不能依賴於頭文件的配置。 在 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
引用cppreference :
“#include 不應在模塊單元中使用(在全局模塊片段之外),因為所有包含的聲明和定義都將被視為模塊的一部分。相反,也可以使用導入聲明來導入標頭:“
和:
“導入頭文件將使其所有定義和聲明都可以訪問。預處理器宏也可以訪問(因為導入聲明被預處理器識別)。但是,與#include 相反,翻譯單元中定義的預處理宏不會影響對頭文件。這在某些情況下可能不方便(某些頭文件使用預處理宏作為配置形式),在這種情況下需要使用全局模塊片段。”
所以日志頭可能包含在全局模塊片段中,但是編譯器需要多次處理它。 如果該標頭正在使用像 spdlog 或類似的第三方庫,則這尤其麻煩,在這種情況下,標頭包含成為遞歸噩夢。
或者,日志記錄頭可能包含在模塊聲明中,但其定義將泄漏到該模塊之外,並且頭文件可能根本不導入任何模塊。 最后一點我嘗試使用 gcc 並得到以下錯誤:
log_helper.hpp:3:1: error: post-module-declaration imports must not be from header inclusion
我們不應該依賴頭文件配置來聲明(和解析)我們的模板化日志函數,因為這是一個非常緩慢的過程,因此我們可以靜態地將配置提供給編譯器(通過實例化一個類型)。 一般的想法是編譯一次模塊,並依賴於模塊的使用者來生成適當的函數調用。
這是您建議的代碼:
template<format_factory F, typename... Args>
static inline void trace(const F& format, Args&&... args) {
std::cout << std::format(format(), map(args)...) << std::endl;
}
如果我們用trace ("arg={}", my_obj);
調用它trace ("arg={}", my_obj);
然后編譯器將為該特定重載生成代碼。 但是,如果我們使用不同數量的參數調用它,例如trace ("arg_1={}, arg_2={}", my_obj1, my_obj2);
,那么編譯器將不得不再次對跟蹤函數進行詞法分析和解析(包括通過依賴模板類型進行遞歸),這是一個緩慢的過程。 對於模塊,我們編譯一次並將其存儲為二進制格式的抽象語法樹,因此之后生成代碼會快得多。
這是我對代碼示例的建議。 我省略了可變參數模板擴展以方便閱讀:
// 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;
};
我將不得不查看優化的程序集(鏈接時優化),以檢查是否優化了空記錄器的間接性。 很可能,一些更有經驗的 C++ 人可以立即分辨出來。
優化的一個想法是為mInstance->Trace()
存儲一個函數指針以避免 vtable 查找。 但同樣,編譯器可能會自動推斷出這一點。 我對該特定主題的經驗非常有限。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.