文章目录
- 一、std::format
- 二、std::source_location
- 三、detail名字空间
- 四、X-macro技术
- 五、cpp20的log
- 参考
一、std::format
GCC 13, CLANG 14 and MSVC 16.10/VS 2019 all have the {fmt} based std::format available in respective standard libraries.
基本字符串格式化:
#include <format>
#include <iostream>int main() {std::string message = std::format("Hello, {}!", "World");std::cout << message << std::endl; // 输出: Hello, World!return 0;
}
多个参数:
#include <format>
#include <iostream>int main() {std::string message = std::format("{}, you have {} new messages", "Alice", 5);std::cout << message << std::endl; // 输出: Alice, you have 5 new messagesreturn 0;
}
指定宽度和填充:
#include <format>
#include <iostream>int main() {std::string message = std::format("{:*>10}", 42);std::cout << message << std::endl; // 输出: *******42return 0;
}
格式说明符
- std::format 支持多种格式说明符,类似于 printf,但更灵活和强大。
整数:
#include <format>
#include <iostream>int main() {std::string dec = std::format("{:d}", 42); // 十进制std::string hex = std::format("{:x}", 42); // 十六进制std::string oct = std::format("{:o}", 42); // 八进制std::cout << "Decimal: " << dec << std::endl; // 输出: Decimal: 42std::cout << "Hexadecimal: " << hex << std::endl; // 输出: Hexadecimal: 2astd::cout << "Octal: " << oct << std::endl; // 输出: Octal: 52return 0;
}
浮点数:
#include <format>
#include <iostream>int main() {std::string fixed = std::format("{:.2f}", 3.14159); // 定点表示,保留两位小数std::string sci = std::format("{:.2e}", 3.14159); // 科学计数法std::cout << "Fixed: " << fixed << std::endl; // 输出: Fixed: 3.14std::cout << "Scientific: " << sci << std::endl; // 输出: Scientific: 3.14e+00return 0;
}
对齐和填充:
#include <format>
#include <iostream>int main() {std::string left = std::format("{:<10}", "left"); // 左对齐std::string right = std::format("{:>10}", "right"); // 右对齐std::string center = std::format("{:^10}", "center"); // 居中对齐std::cout << "Left: " << left << std::endl; // 输出: Left: left std::cout << "Right: " << right << std::endl; // 输出: Right: rightstd::cout << "Center: " << center << std::endl; // 输出: Center: center return 0;
}
指定填充字符:
#include <format>
#include <iostream>int main() {std::string padded = std::format("{:*>10}", 42); // 用 '*' 填充,右对齐std::cout << padded << std::endl; // 输出: *******42return 0;
}
套格式化:
- 你可以将格式化字符串作为参数传递,进行嵌套格式化:
#include <format>
#include <iostream>int main() {std::string nested = std::format("Result: {}", std::format("{:.2f}", 3.14159));std::cout << nested << std::endl; // 输出: Result: 3.14return 0;
}
自定义类型格式化:
- 你可以通过定义 formatter 特化来格式化自定义类型:
- 为 Point 结构体特化 std::formatter 模板。特化的模板需要实现两个函数:parse 和 format。
#include <format>
#include <iostream>// 定义 Point 结构体
struct Point {int x, y;
};// 为 Point 特化 std::formatter 模板
template <>
struct std::formatter<Point> {// 支持的格式说明符constexpr auto parse(format_parse_context& ctx) {// 这里你可以解析特定的格式说明符,如果有的话auto it = ctx.begin();auto end = ctx.end();if (it != end && (*it == 'f' || *it == 'd')) {++it;}// 检查格式字符串是否正确if (it != end && *it != '}') {throw format_error("invalid format");}// 返回格式说明符的结束位置return it;}// 格式化函数auto format(const Point& p, format_context& ctx) const {// 格式化输出return format_to(ctx.out(), "({}, {})", p.x, p.y);}
};int main() {Point p{1, 2};std::string pointStr = std::format("Point: {}", p);std::cout << pointStr << std::endl; // 输出: Point: (1, 2)return 0;
}
二、std::source_location
std::source_location 提供了一种获取编译时源代码位置信息的便捷方法;
std::source_location 是 C++20 引入的一个标准库特性,用于获取代码的编译时信息,如文件名、行号、列号和函数名。这对于调试和日志记录非常有用,因为它可以在运行时捕获这些信息,而不需要手动提供。
基本用法
- std::source_location 类似于传统的预处理器宏(如 __FILE__ 和 __LINE__),但提供了更灵活和安全的接口。
#include <iostream>
#include <source_location>void logMessage(const std::string& message, const std::source_location& location = std::source_location::current()) {std::cout << "Message: " << message << "\n"<< "File: " << location.file_name() << "\n"<< "Line: " << location.line() << "\n"<< "Column: " << location.column() << "\n"<< "Function: " << location.function_name() << "\n";
}int main() {logMessage("This is a log message");return 0;
}
std::source_location 类:
- std::source_location 是一个不可变(immutable)的类,它包含了与源代码位置相关的信息。
- 常用的成员函数有:
file_name(): 返回当前文件名的字符串。
line(): 返回当前行号。
column(): 返回当前列号。
function_name(): 返回当前函数名。
在 logMessage 函数中,location 参数的默认值是 std::source_location::current(),它捕获调用 logMessage 时的源代码位置。 这使得调用者无需显式提供源代码位置,编译器会自动提供。
自定义日志宏
- 你可以定义一个宏来简化日志记录,利用 std::source_location 捕获源代码位置。
#include <iostream>
#include <source_location>#define LOG_MESSAGE(msg) logMessage(msg)void logMessage(const std::string& message, const std::source_location& location = std::source_location::current()) {std::cout << "Message: " << message << "\n"<< "File: " << location.file_name() << "\n"<< "Line: " << location.line() << "\n"<< "Column: " << location.column() << "\n"<< "Function: " << location.function_name() << "\n";
}int main() {LOG_MESSAGE("This is a log message");return 0;
}
捕获异常位置
- 可以在异常处理时使用 std::source_location 捕获抛出异常的位置,从而提供更详细的错误信息。
#include <iostream>
#include <stdexcept>
#include <source_location>void throwError(const std::string& message, const std::source_location& location = std::source_location::current()) {throw std::runtime_error(std::format("Error: {}\nFile: {}\nLine: {}\nColumn: {}\nFunction: {}",message, location.file_name(), location.line(), location.column(), location.function_name()));
}int main() {try {throwError("An example error");} catch (const std::runtime_error& e) {std::cerr << e.what() << std::endl;}return 0;
}
三、detail名字空间
details名字空间实现内部函数的封装,对外隐藏细节,只暴露必要API函数
#include <condition_variable>
#include <functional>
#include <future>
#include <iostream>
#include <mutex>
#include <queue>
#include <stdexcept>
#include <thread>
#include <utility>
#include <vector>class ThreadPool {public:explicit ThreadPool(size_t numThreads);~ThreadPool();template <class F, class... Args>auto enqueue(F&& f, Args&&... args)-> std::future<typename std::result_of<F(Args...)>::type>;private:// Worker threadsstd::vector<std::thread> workers;// Task queuestd::queue<std::function<void()>> tasks;// Synchronizationstd::mutex queueMutex;std::condition_variable condition;bool stop;// Internal implementation detailsvoid worker();
};// Implementation of ThreadPool methodsThreadPool::ThreadPool(size_t numThreads) : stop(false) {for (size_t i = 0; i < numThreads; ++i) {workers.emplace_back(&ThreadPool::worker, this);}
}ThreadPool::~ThreadPool() {{std::unique_lock<std::mutex> lock(queueMutex);stop = true;}condition.notify_all();for (std::thread& worker : workers) {worker.join();}
}template <class F, class... Args>
auto ThreadPool::enqueue(F&& f, Args&&... args)-> std::future<typename std::result_of<F(Args...)>::type> {using return_type = typename std::result_of<F(Args...)>::type;//创建一个 std::packaged_task 对象并包装一个可调用对象(任务)//它会将任务的结果保存到一个与之关联的 std::promise 对象中。auto task = std::make_shared<std::packaged_task<return_type()>>(std::bind(std::forward<F>(f), std::forward<Args>(args)...));std::future<return_type> res = task->get_future();{std::unique_lock<std::mutex> lock(queueMutex);if (stop) {throw std::runtime_error("enqueue on stopped ThreadPool");}tasks.emplace([task]() { (*task)(); });}condition.notify_one();return res;
}void ThreadPool::worker() {while (true) {std::function<void()> task;{std::unique_lock<std::mutex> lock(queueMutex);condition.wait(lock, [this] { return stop || !tasks.empty(); });if (stop && tasks.empty()) {return;}task = std::move(tasks.front());tasks.pop();}task();}
}int main() {ThreadPool pool(4);auto result = pool.enqueue([](int answer) { return answer; }, 42);std::cout << "The answer is " << result.get() << std::endl;return 0;
}
测试:
Program returned: 0
Program stdout
The answer is 42
std::packaged_task 和 std::future 的结合使用的例子:
#include <iostream>
#include <future>
#include <thread>
#include <chrono>// 一个简单的任务函数,返回输入值的两倍
int doubleValue(int x) {std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟长时间任务return x * 2;
}int main() {// 创建一个 std::packaged_task 对象并包装 doubleValue 函数std::packaged_task<int(int)> task(doubleValue);// 获取与该任务关联的 std::future 对象std::future<int> result = task.get_future();// 启动一个线程来异步执行该任务std::thread t(std::move(task), 10);// 在主线程中可以做其他工作// 等待任务完成并获取结果std::cout << "Result: " << result.get() << std::endl;// 等待线程完成t.join();return 0;
}
std::promise 和 std::future 结合使用的例子:
#include <iostream>
#include <future>
#include <thread>
#include <chrono>// 一个简单的任务函数,返回输入值的两倍
int doubleValue(int x) {std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟长时间任务return x * 2;
}int main() {// 创建一个 std::promise 对象std::promise<int> promise;// 获取与该 promise 关联的 std::future 对象std::future<int> result = promise.get_future();// 启动一个线程来异步执行任务,并使用 lambda 设置 promise 的值std::thread t([&promise](int x) {// 执行任务并设置 promise 的值promise.set_value(doubleValue(x));}, 10);// 在主线程中可以做其他工作// 等待任务完成并获取结果std::cout << "Result: " << result.get() << std::endl;// 等待线程完成t.join();return 0;
}
四、X-macro技术
X-macro技术的基本思想是使用预处理器宏来定义一组数据或操作,然后通过另一个宏来实际展开这些数据或操作。
eg:假设我们有一组颜色定义,并希望使用这些定义来生成枚举和字符串数组。传统的做法需要分别定义这些内容,可能导致重复代码。
X-macro技术可以帮助我们解决这个问题。
#include <stdio.h>//首先,定义一个包含所有颜色的宏列表:
#define COLOR_LIST \X(RED) \X(GREEN) \X(BLUE) \X(YELLOW)//接下来,使用这个宏列表生成枚举
//这里,我们定义了一个宏 X,它会在 COLOR_LIST 中被展开,每个颜色都会生成相应的枚举值。
typedef enum {#define X(color) color,COLOR_LIST#undef X
} Color;//然后,可以使用相同的宏列表生成字符串数组:
const char* ColorNames[] = {#define X(color) #color,COLOR_LIST#undef X
};int main() {for (int i = 0; i < sizeof(ColorNames)/sizeof(ColorNames[0]); ++i) {printf("Color %d: %s\n", i, ColorNames[i]);}return 0;
}
测试:
Program returned: 0
Program stdout
Color 0: RED
Color 1: GREEN
Color 2: BLUE
Color 3: YELLOW
五、cpp20的log
旧的打印方法:
#include <format>
#include <source_location>
#include <iostream>void log(std::string msg, const char* file, int line)
{std::cout<< file << ":"<<line<< " [Info] "<<msg<<'\n';
}#define LOG(msg) log(msg,__FILE__,__LINE__)int main()
{LOG("wangji");return 0;
}
使用source_location,source_location写在默认参数的位置(内部实现buildlocation),实际是打印时是调用者的信息
#include <format>
#include <source_location>
#include <iostream>void log(std::string msg, std::source_location loc=std::source_location::current())
{std::cout<< loc.file_name() << ":"<<loc.line()<< " [Info] "<<msg<<'\n';
}#define LOG(msg) log(msg)int main()
{LOG("wangji");return 0;
}
结合std::format,把format的参数抄过来
#include <format>
#include <iostream>
#include <source_location>template <class T>
struct with_source_location {private:T inner;std::source_location loc;public:template <class U>requires std::constructible_from<T, U>consteval with_source_location(U &&inner, std::source_location loc = std::source_location::current()): inner(std::forward<U>(inner)), loc(std::move(loc)) {}constexpr T const &format() const { return inner; }constexpr std::source_location const &location() const { return loc; }
};template <typename... Args>
void log_info(with_source_location<std::format_string<Args...>> fmt,Args &&...args) {auto const &loc = fmt.location();std::cout << loc.file_name() << ":" << loc.line() << " [Info] "<< std::vformat(fmt.format().get(),std::make_format_args(args...))<< '\n';
}int main() {log_info("wangji1999");return 0;
}
测试:
Program returned: 0
Program stdout
/app/example.cpp:35 [Info] wangji1999
使用X macro技术来定义日志等级
很神奇的是,最后宏展开后,最后一个枚举后面没有增加逗号
#include <format>
#include <iostream>
#include <source_location>
#include <cstdint>#define MINILOG_FOREACH_LOG_LEVEL(f) \f(trace) \f(debug) \f(info) \f(critical) \f(warn) \f(error) \f(fatal)enum class log_level : std::uint8_t {
#define _FUNCTION(name) name,MINILOG_FOREACH_LOG_LEVEL(_FUNCTION)
#undef _FUNCTION
};//cpp17支持inline修饰全局唯一的全局变量
inline std::string log_level_name(log_level lev) {switch (lev) {
#define _FUNCTION(name) case log_level::name: return #name;MINILOG_FOREACH_LOG_LEVEL(_FUNCTION)
#undef _FUNCTION}return "unknown";
}template <class T>
struct with_source_location {private:T inner;std::source_location loc;public://consteval只能编译器调用,constexpr既可以是编译期也可以是运行期调用template <class U>requires std::constructible_from<T, U>consteval with_source_location(U &&inner, std::source_location loc = std::source_location::current()): inner(std::forward<U>(inner)), loc(std::move(loc)) {}constexpr T const &format() const { return inner; }constexpr std::source_location const &location() const { return loc; }
};//cpp17支持inline修饰全局唯一的全局变量
inline log_level max_level=log_level::info;template <typename... Args>
void generic_log(log_level lev, with_source_location<std::format_string<Args...>> fmt,Args &&...args)
{if (lev>= max_level){auto const &loc = fmt.location();std::cout << loc.file_name() << ":" << loc.line() << " [Info] "<< std::vformat(fmt.format().get(),std::make_format_args(args...))<< '\n';}
}//X macro技术封装不同等级的日志函数
#define _FUNCTION(name) \
template <typename... Args> \
void log_##name(with_source_location<std::format_string<Args...>> fmt, Args &&...args) { \return generic_log(log_level::name, std::move(fmt), std::forward<Args>(args)...); \
}
MINILOG_FOREACH_LOG_LEVEL(_FUNCTION)
#undef _FUNCTIONint main() {generic_log(log_level::debug ,"wangji {}", "hi");generic_log(log_level::info ,"wangji {}", "hi");log_debug( "wangji {}", "hi");log_info("wangji {}", "hi");return 0;
}
注意:
全局的模板函数和直接在类内定义的成员函数,cpp自动给你加上inline
为了防止多个log模块冲突,需要加上namespace,注意宏是怎么加的,要在内部加;
不想暴露给用户的函数,用details的namespce包起来,但是用户仍然可以拿到
#include <format>
#include <iostream>
#include <source_location>
#include <cstdint>
#include <string>
#include <fstream>
#include <chrono>namespace minilog{#define MINILOG_FOREACH_LOG_LEVEL(f) \f(trace) \f(debug) \f(info) \f(critical) \f(warn) \f(error) \f(fatal)enum class log_level : std::uint8_t {
#define _FUNCTION(name) name,MINILOG_FOREACH_LOG_LEVEL(_FUNCTION)
#undef _FUNCTION
};//不想暴露给用户的函数,用details的namespce包起来,但是用户仍然可以拿到
namespace detail{//给日志设置颜色,ansi控制码,1m表示强调色
//\E,\033是一样的
#if defined(__linux__) || defined(__APPLE__)
inline constexpr char k_level_ansi_colors[(std::uint8_t)log_level::fatal + 1][8] = {"\E[37m","\E[35m","\E[32m","\E[34m","\E[33m","\E[31m","\E[31;1m",
};
inline constexpr char k_reset_ansi_color[4] = "\E[m";
#define _MINILOG_IF_HAS_ANSI_COLORS(x) x
#else
#define _MINILOG_IF_HAS_ANSI_COLORS(x)
inline constexpr char k_level_ansi_colors[(std::uint8_t)log_level::fatal + 1][1] = {"","","","","","","",
};
inline constexpr char k_reset_ansi_color[1] = "";
#endifinline std::string log_level_name(log_level lev) {switch (lev) {
#define _FUNCTION(name) case log_level::name: return #name;MINILOG_FOREACH_LOG_LEVEL(_FUNCTION)
#undef _FUNCTION}return "unknown";
}inline log_level log_level_from_name(std::string lev){
#define _FUNCTION(name) if (lev == #name) return log_level::name;MINILOG_FOREACH_LOG_LEVEL(_FUNCTION)
#undef _FUNCTIONreturn log_level::info;
}template <class T>
struct with_source_location {private:T inner;std::source_location loc;public:template <class U>requires std::constructible_from<T, U>consteval with_source_location(U &&inner, std::source_location loc = std::source_location::current()): inner(std::forward<U>(inner)), loc(std::move(loc)) {}constexpr T const &format() const { return inner; }constexpr std::source_location const &location() const { return loc; }
};//通过环境变量设置loglevel
inline log_level g_max_level=[]()->log_level{auto lev=std::getenv("MINILOG_LEVEL");if (lev){return log_level_from_name(lev);}else{return log_level::info;}
}();//自定义输出文件
inline std::ofstream g_log_file=[]()->std::ofstream{auto path=std::getenv("MINILOG_FILE");if (path){return std::ofstream(path, std::ios::app);}else{return std::ofstream();}
}();inline void output_log(log_level lev, std::string msg, std::source_location const &loc) {//增加时间戳,cpp20std::chrono::zoned_time now{std::chrono::current_zone(), std::chrono::high_resolution_clock::now()};msg = std::format("{} {}:{} [{}] {}", now, loc.file_name(), loc.line(), log_level_name(lev), msg);if (g_log_file) {g_log_file << msg + '\n';}if (lev >= detail::g_max_level) {std::cout << _MINILOG_IF_HAS_ANSI_COLORS(k_level_ansi_colors[(std::uint8_t)lev] +)msg _MINILOG_IF_HAS_ANSI_COLORS(+ k_reset_ansi_color) + '\n';}
}}//追加写
inline void set_log_file(std::string path) {detail::g_log_file = std::ofstream(path, std::ios::app);
}//一般不直接暴露变量给外面,而是用过某个函数设置日志等级
inline void set_log_level(log_level lev){detail::g_max_level = lev;
}template <typename... Args>
void generic_log(log_level lev, detail::with_source_location<std::format_string<Args...>> fmt,Args &&...args)
{if (lev>= detail::g_max_level){auto const &loc = fmt.location();//cout的线程安全问题,多次使用cout可能不是线程安全的,一次使用则是线程安全的std::cout _MINILOG_IF_HAS_ANSI_COLORS(<< detail::k_level_ansi_colors[(std::uint8_t)lev])<< loc.file_name() << ":" << loc.line() <<" [" <<detail::log_level_name(lev)<< "] "<< std::vformat(fmt.format().get(), std::make_format_args(args...))_MINILOG_IF_HAS_ANSI_COLORS(<< detail::k_reset_ansi_color)<< '\n';}
}#define _FUNCTION(name) \
template <typename... Args> \
void log_##name(detail::with_source_location<std::format_string<Args...>> fmt, Args &&...args) { \return generic_log(log_level::name, std::move(fmt), std::forward<Args>(args)...); \
}
MINILOG_FOREACH_LOG_LEVEL(_FUNCTION)
#undef _FUNCTION//直接用这个宏,这个宏不遵守namspace,定义宏的时候,日志宏名称的前缀使用namespace作为前缀
#define MINILOG_P(x) ::minilog::log_info(#x "={}", x)}int main() {MINILOG_P("100");::minilog::log_fatal("123");::minilog::log_debug("123");minilog::set_log_level(minilog::log_level::trace); // default log level is info::minilog::log_debug("123");std::cout<<"============"<<std::endl;#define _FUNCTION(name) minilog::log_##name(#name);MINILOG_FOREACH_LOG_LEVEL(_FUNCTION)#undef _FUNCTIONreturn 0;
}
测试:
参考
- 【C++20工程实战】自己动手实现纯头文件日志库
- 项目源码