在现代 C++20 标准中,std::format 提供了一种类型安全且语法优雅的字符串格式化机制,完美取代了传统的 printf 风格。利用它可以轻松构建一个可插拔、可配置的日志系统,支持多种输出目标(控制台、文件、网络)和日志级别。下面给出一个完整示例,展示如何定义日志级别枚举、构建线程安全的日志器、实现可定制化的格式化模板,并演示多线程环境下的使用。
1. 日志级别枚举与字符串映射
#include <string_view>
#include <unordered_map>
enum class LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
Fatal
};
constexpr std::unordered_map<LogLevel, std::string_view> LogLevelNames = {
{LogLevel::Trace, "TRACE"},
{LogLevel::Debug, "DEBUG"},
{LogLevel::Info, "INFO" },
{LogLevel::Warn, "WARN" },
{LogLevel::Error, "ERROR"},
{LogLevel::Fatal, "FATAL"}
};
inline std::string_view to_string(LogLevel level) {
return LogLevelNames.at(level);
}
2. 输出目标基类与具体实现
#include <ostream>
#include <memory>
#include <mutex>
#include <fstream>
#include <iostream>
class LogSink {
public:
virtual ~LogSink() = default;
virtual void write(const std::string& msg) = 0;
};
class ConsoleSink : public LogSink {
public:
void write(const std::string& msg) override {
std::lock_guard<std::mutex> lock(mutex_);
std::cout << msg << '\n';
}
private:
std::mutex mutex_;
};
class FileSink : public LogSink {
public:
explicit FileSink(const std::string& filename) : file_(filename, std::ios::app) {}
void write(const std::string& msg) override {
std::lock_guard<std::mutex> lock(mutex_);
file_ << msg << '\n';
}
private:
std::ofstream file_;
std::mutex mutex_;
};
3. 日志器核心实现
#include <format>
#include <chrono>
#include <iomanip>
#include <vector>
class Logger {
public:
Logger() : level_(LogLevel::Info), formatTemplate_("[{timestamp}] [{level}] {message}") {}
void setLevel(LogLevel lvl) { level_ = lvl; }
void setFormat(std::string_view tmpl) { formatTemplate_ = tmpl; }
void addSink(std::shared_ptr <LogSink> sink) {
std::lock_guard<std::mutex> lock(sinkMutex_);
sinks_.push_back(std::move(sink));
}
template<typename... Args>
void log(LogLevel lvl, std::string_view fmt, Args&&... args) {
if (lvl < level_) return;
std::string formattedMsg = std::vformat(fmt, std::make_format_args(args...));
std::string finalMsg = formatMessage(lvl, formattedMsg);
writeToSinks(finalMsg);
}
// Convenience wrappers
template<typename... Args>
void trace(std::string_view fmt, Args&&... args) { log(LogLevel::Trace, fmt, std::forward <Args>(args)...); }
template<typename... Args>
void debug(std::string_view fmt, Args&&... args) { log(LogLevel::Debug, fmt, std::forward <Args>(args)...); }
template<typename... Args>
void info(std::string_view fmt, Args&&... args) { log(LogLevel::Info, fmt, std::forward <Args>(args)...); }
template<typename... Args>
void warn(std::string_view fmt, Args&&... args) { log(LogLevel::Warn, fmt, std::forward <Args>(args)...); }
template<typename... Args>
void error(std::string_view fmt, Args&&... args) { log(LogLevel::Error, fmt, std::forward <Args>(args)...); }
template<typename... Args>
void fatal(std::string_view fmt, Args&&... args) { log(LogLevel::Fatal, fmt, std::forward <Args>(args)...); }
private:
std::string formatMessage(LogLevel lvl, const std::string& msg) {
auto now = std::chrono::system_clock::now();
std::time_t tt = std::chrono::system_clock::to_time_t(now);
std::tm tm;
#if defined(_MSC_VER)
localtime_s(&tm, &tt);
#else
localtime_r(&tt, &tm);
#endif
std::ostringstream oss;
oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
std::string timestamp = oss.str();
std::string result = formatTemplate_;
replaceAll(result, "{timestamp}", timestamp);
replaceAll(result, "{level}", std::string(to_string(lvl)));
replaceAll(result, "{message}", msg);
return result;
}
void writeToSinks(const std::string& msg) {
std::lock_guard<std::mutex> lock(sinkMutex_);
for (auto& sink : sinks_) {
sink->write(msg);
}
}
// Simple string replace helper
static void replaceAll(std::string& str, const std::string& from, const std::string& to) {
if (from.empty()) return;
size_t pos = 0;
while ((pos = str.find(from, pos)) != std::string::npos) {
str.replace(pos, from.length(), to);
pos += to.length();
}
}
LogLevel level_;
std::string formatTemplate_;
std::vector<std::shared_ptr<LogSink>> sinks_;
std::mutex sinkMutex_;
};
4. 使用示例
#include <thread>
#include <vector>
#include <chrono>
int main() {
Logger logger;
logger.setLevel(LogLevel::Debug);
logger.setFormat("[{timestamp}] [{level}] {message}");
logger.addSink(std::make_shared <ConsoleSink>());
logger.addSink(std::make_shared <FileSink>("app.log"));
logger.info("程序启动,线程数 {}", std::thread::hardware_concurrency());
// 启动多线程演示
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back([&, i]() {
for (int j = 0; j < 10; ++j) {
logger.debug("线程 {} 计数 {}", i, j);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
});
}
for (auto& t : threads) t.join();
logger.info("程序结束");
}
运行效果(控制台)
[2026-01-09 12:34:56] [INFO] 程序启动,线程数 8
[2026-01-09 12:34:56] [DEBUG] 线程 0 计数 0
[2026-01-09 12:34:56] [DEBUG] 线程 1 计数 0
...
[2026-01-09 12:35:05] [INFO] 程序结束
日志文件 app.log(与控制台内容相同)
[2026-01-09 12:34:56] [INFO] 程序启动,线程数 8
[2026-01-09 12:34:56] [DEBUG] 线程 0 计数 0
[2026-01-09 12:34:56] [DEBUG] 线程 1 计数 0
...
[2026-01-09 12:35:05] [INFO] 程序结束
5. 可扩展性与改进
- 异步日志:将日志写入队列,后台线程批量写入文件或网络,进一步降低 I/O 阻塞。
- 滚动文件:在
FileSink中实现文件大小或日期滚动,避免单文件过大。 - 网络输出:实现
NetworkSink,通过 TCP/UDP 将日志发送至日志收集中心。 - 配置文件:从 JSON/YAML 文件读取日志级别、格式、sink 列表,支持热重载。
- 多进程共享:利用共享内存 + 进程间消息队列实现跨进程日志收集。
6. 小结
C++20 的 std::format 为日志系统提供了强大且类型安全的格式化能力,使得构建可读、可维护的日志变得异常简单。通过上述设计,你可以轻松将日志系统集成到任何项目中,并在需要时按需扩展输出目标或格式化策略。祝编码愉快!