【C++20工程实战】自己动手实现纯头文件日志库

文章目录

一、std::format

GCC 13, CLANG 14 and MSVC 16.10/VS 2019 all have the {fmt} based std::format available in respective standard libraries.

基本字符串格式化:

cpp 复制代码
#include <format>
#include <iostream>

int main() {
    std::string message = std::format("Hello, {}!", "World");
    std::cout << message << std::endl;  // 输出: Hello, World!
    return 0;
}

多个参数:

cpp 复制代码
#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 messages
    return 0;
}

指定宽度和填充:

cpp 复制代码
#include <format>
#include <iostream>

int main() {
    std::string message = std::format("{:*>10}", 42);
    std::cout << message << std::endl;  // 输出: *******42
    return 0;
}

格式说明符

  • std::format 支持多种格式说明符,类似于 printf,但更灵活和强大。

整数:

cpp 复制代码
#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: 42
    std::cout << "Hexadecimal: " << hex << std::endl;  // 输出: Hexadecimal: 2a
    std::cout << "Octal: " << oct << std::endl;  // 输出: Octal: 52
    return 0;
}

浮点数:

cpp 复制代码
#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.14
    std::cout << "Scientific: " << sci << std::endl;  // 输出: Scientific: 3.14e+00
    return 0;
}

对齐和填充:

cpp 复制代码
#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:     right
    std::cout << "Center: " << center << std::endl;  // 输出: Center:   center  
    return 0;
}

指定填充字符:

cpp 复制代码
#include <format>
#include <iostream>

int main() {
    std::string padded = std::format("{:*>10}", 42);  // 用 '*' 填充,右对齐
    std::cout << padded << std::endl;  // 输出: *******42
    return 0;
}

套格式化:

  • 你可以将格式化字符串作为参数传递,进行嵌套格式化:
cpp 复制代码
#include <format>
#include <iostream>

int main() {
    std::string nested = std::format("Result: {}", std::format("{:.2f}", 3.14159));
    std::cout << nested << std::endl;  // 输出: Result: 3.14
    return 0;
}

自定义类型格式化:

  • 你可以通过定义 formatter 特化来格式化自定义类型:
  • 为 Point 结构体特化 std::formatter 模板。特化的模板需要实现两个函数:parse 和 format。
cpp 复制代码
#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 类似于传统的预处理器宏(如 FILELINE),但提供了更灵活和安全的接口。
cpp 复制代码
#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 捕获源代码位置。
cpp 复制代码
#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 捕获抛出异常的位置,从而提供更详细的错误信息。
cpp 复制代码
#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函数

cpp 复制代码
#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 threads
    std::vector<std::thread> workers;
    // Task queue
    std::queue<std::function<void()>> tasks;

    // Synchronization
    std::mutex queueMutex;
    std::condition_variable condition;
    bool stop;

    // Internal implementation details
    void worker();
};

// Implementation of ThreadPool methods

ThreadPool::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;
}

测试:

bash 复制代码
Program returned: 0
Program stdout
The answer is 42

std::packaged_task 和 std::future 的结合使用的例子:

cpp 复制代码
#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 结合使用的例子:

cpp 复制代码
#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技术可以帮助我们解决这个问题。

cpp 复制代码
#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;
}

测试:

bash 复制代码
Program returned: 0
Program stdout
Color 0: RED
Color 1: GREEN
Color 2: BLUE
Color 3: YELLOW

五、cpp20的log

旧的打印方法:

cpp 复制代码
#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),实际是打印时是调用者的信息

cpp 复制代码
#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的参数抄过来

cpp 复制代码
#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;
}

测试:

cpp 复制代码
Program returned: 0
Program stdout
/app/example.cpp:35 [Info] wangji1999

使用X macro技术来定义日志等级

很神奇的是,最后宏展开后,最后一个枚举后面没有增加逗号

cpp 复制代码
#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 _FUNCTION


int 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包起来,但是用户仍然可以拿到

cpp 复制代码
#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] = "";
#endif


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";
}


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 _FUNCTION
    return 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) {
    //增加时间戳,cpp20
    std::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 _FUNCTION
    return 0;
}

测试:

参考

相关推荐
繁星十年2 天前
在C++中,工厂模式的思考(《C++20设计模式》及常规设计模式对比)
c++·设计模式·c++20
繁星十年8 天前
在C++中,构造器(Builder)模式的思考(《C++20设计模式》及常规设计模式对比)
c++·设计模式·c++20
程序猿阿伟23 天前
PHP 中如何高效地处理大规模数据的排序?
c++20
zhangzhangkeji1 个月前
vs2019 c++20 规范 STL库中关于时间的模板
c++·c++20
DogDaoDao2 个月前
c++ 各版本特性介绍
c++·c++11·c++20·c++14·c++17·c++03
vczxh2 个月前
c++20 std::reinterpret_cast、std::bit_cast、std::static_cast
c++20
zhangzhangkeji2 个月前
vs2019 里 C++ 20规范的 string 类的源码注释
c++20
别NULL2 个月前
探究C++20协程(5)——基于挂起实现无阻塞的定时器
算法·c++20
+xiaowenhao+2 个月前
《Beginning C++20 From Novice to Professional》第五章 Arrays and Loops
开发语言·c++·c++20