[岩禾溪] C++20项目 muduo网络库 项目实战 (1)Logger & Timestamp

发布时间 2023-11-25 10:03:10作者: 岩禾溪

  编辑本项目由 岩禾溪 原创

 

项目实战+新特性用法介绍
开源代码+博客解析+视频讲解

 

GitHub+CSDN+BiliBili同步更新,三个平台同名【岩禾溪】

视频讲解和代码链接在文章末尾,你的关注是我更新的最大动力

项目环境

本项目采用C++20开发 精简Muduo网络库

Build Tool:Xmake

archlinux-2023.09.01-x86_64

gcc version 13.2.1 20230801 (GCC)

xmake

构建工具简介 xmake 是一个灵活且功能强大的构建工具,支持多种编程语言,特别适用于 C/C++ 项目。

xmake.lua 配置文件

target("cpp")
    set_kind("binary")
    add_files("*.ixx","*cpp")
    set_languages("c++20")

xmake.lua 是 xmake 的配置文件,在这里定义了项目的构建规则和参数。 项目构建规则

target("cpp"):定义项目名称。 set_kind("binary"):指定构建生成的是可执行文件。 add_files(".ixx", "cpp"):添加项目源文件。 set_languages("c++20"):设置使用 C++20 标准编译代码。 C++20 支持

通过 set_languages("c++20"),我们启用了 C++20 标准的支持,使得项目可以利用最新的 C++ 特性。 简洁高效的构建

xmake 简化了构建流程,使得项目的构建过程更加简洁高效。

nocopyable

export module nocopyable;

export class nocopyable{
    public:
    nocopyable(const nocopyable&) = delete;
    nocopyable& operator=(const nocopyable&)   = delete;

    protected:

    nocopyable() = default;
    ~nocopyable() = default;
};
  1. 模块定义

    使用 export module nocopyable; 定义了一个名为 nocopyable 的 C++20 模块。
  2. 禁止复制

    nocopyable 类中的 nocopyable(const nocopyable&) = delete;nocopyable& operator=(const nocopyable&) = delete; 阻止了对象的复制和赋值。通过删除拷贝构造函数和拷贝赋值运算符,该类确保不会被复制创建或赋值。
  3. 受保护的构造函数和析构函数

    构造函数和析构函数声明为 protected,这意味着它们只能被该类及其派生类内部访问。这使得该类可以被继承,但不允许直接创建类的对象或对其进行复制。

nocopyable 模块是一种常见的技术,用于设计不希望被复制或赋值的类,例如单例模式的类或管理资源的类。

Logger

Logger.cpp

import Logger;
import Timestamp;
import <iostream>;
import <format>;
// 获取日志唯一的实例对象
Logger &Logger::instance()
{
    static Logger logger;
    return logger;
}

// 设置日志级别
void Logger::setLogLevel(LogLevel level)
{
    logLevel_ = level;
}
template <typename... Args>
    requires(std::convertible_to<Args, std::string> && ...)
void Logger::log(LogLevel level,
                 const std::string &format,
                 Args &&...args)
{
    // 设置日志级别
    setLogLevel(level);

    // 构建日志消息可以无所谓,不能无所获
    std::string message = std::format("[{}] {} : {}", level, Timestamp::now().toString(), std::format(format, std::forward<Args>(args)...));

    // 打印日志消息到控制台
    std::cout << message << std::endl;
}

这段代码展示了一个简单的日志记录器 Logger 的部分实现,利用模块化方式组织。这个模块包含了日志记录的实例获取、日志级别设置和日志记录功能。

  1. 模块导入

    import Logger; 语句导入了 Logger 模块
  2. 获取日志实例对象

    Logger::instance() 函数返回唯一的日志实例。
  3. 设置日志级别

    Logger::setLogLevel(LogLevel level) 函数用于设置日志级别。
  4. 日志记录功能

    Logger::log() 函数实现了日志记录的功能,根据传入的日志级别和格式进行记录。

Logger.ixx

export module Logger;
import <string>;
import nocopyable;
import <concepts>;
export class Logger : public nocopyable
{
public:
    enum class LogLevel
    {
        INFO,  // 普通信息
        ERROR, // 错误信息
        FATAL, // core信息
        DEBUG, // 调试信息
    };
    static Logger &instance();
    void setLogLevel(LogLevel level);

    template <typename... Args>
        requires(std::convertible_to<Args, std::string> && ...)
    void log(LogLevel level, const std::string &format, Args &&...args);

private:
    LogLevel logLevel_ = LogLevel::INFO;
};
  1. 模块化定义

    通过 export module Logger; 定义了一个名为 Logger 的 C++20 模块,但前面的 module; 是无效的。
  2. 模块导入

    import <string>;import nocopyable; 语句导入了相应的模块和头文件。
  3. 日志记录器类

    Logger 类是一个日志记录器类,继承了 nocopyable,表明该类不可被复制。
  4. 日志级别定义

    enum class LogLevel 定义了几个日志级别:INFO、ERROR、FATAL、DEBUG。
  5. 获取日志实例和设置日志级别

    instance() 函数用于获取日志唯一的实例对象。setLogLevel() 函数用于设置日志级别。
  6. 日志记录功能

    log() 函数实现了根据传入的日志级别、格式和参数进行日志记录的功能。
template <typename... Args>
        requires(std::convertible_to<Args, std::string> && ...)
    void log(LogLevel level, const std::string &format, Args &&...args);

在这段代码中,使用了C++20中的模板约束(template constraints)特性。模板约束允许你对模板参数进行更精细的限制,以确保模板参数满足特定的条件。在你提供的函数签名中,使用了 requires 关键字并结合了 std::convertible_to 的概念约束(concept constraint)。

让我们来解释这个函数签名中的不同部分:

  • template <typename... Args>:这声明了一个模板函数,它接受零个或多个模板参数,并将它们命名为 Args

  • requires:这是C++20中的关键字,用于引入模板约束。它后面紧跟着模板约束表达式,用于对模板参数进行约束。

  • (std::convertible_to<Args, std::string> && ...):这是模板约束表达式的一部分。std::convertible_to 是C++20中的概念(concept),用于检查类型是否可以转换为另一种类型。在这里,std::convertible_to<Args, std::string> 检查模板参数 Args 是否可以隐式转换为 std::string 类型。&& ... 是展开运算符,表示对参数包中的每个元素应用这个概念。

因此,这个模板函数 log 只允许接受满足以下条件的参数:

  • 模板参数 Args 的每个类型必须能够隐式转换为 std::string 类型。

这样的约束可以确保在调用 log 函数时,传递的参数能够隐式转换为 std::string,这符合函数签名中 format 参数为 const std::string & 类型的要求。

Timestamp

Timestamp.ixx

export module Timestamp;
export import <string>;
// 时间类
export class Timestamp
{
public:
    Timestamp();
    explicit Timestamp(int64_t microSecondsSinceEpoch);
    static Timestamp now();
    std::string toString() const;

private:
    int64_t microSecondsSinceEpoch_;
};
  1. 模块化定义

    通过 export module Timestamp; 定义了一个名为 Timestamp 的 C++20 模块,但前面的 module; 是无效的,应该指定模块的名称。
  2. 头文件包含

    #include <cstdint>#include <string>:这些是标准头文件,可能用于定义时间类所需的类型和字符串处理。
  3. 时间类定义

    Timestamp
    • 类定义了一个时间类,封装了一些关于时间处理的功能:

    • std::string toString() const 返回时间的字符串表示形式。

    • 静态函数 static Timestamp now() 用于获取当前时间;

    • 参数构造函数 explicit Timestamp(int64_t microSecondsSinceEpoch)

    • 默认构造函数 Timestamp()

 

Timestamp.cpp

import Timestamp;
import <ctime>;
Timestamp::Timestamp() : microSecondsSinceEpoch_(0) {}

Timestamp::Timestamp(int64_t microSecondsSinceEpoch) : microSecondsSinceEpoch_(microSecondsSinceEpoch) {}

Timestamp Timestamp::now()
{
    return Timestamp(time(nullptr));
}

std::string Timestamp::toString() const
{
    char buf[128] = {0};
    time_t timeValue = static_cast<time_t>(microSecondsSinceEpoch_ / 1000000);
    std::tm result;

    if (localtime_r(&timeValue, &result) != nullptr)
    {
        snprintf(buf, 128, "%4d/%02d/%02d %02d:%02d:%02d",
                 result.tm_year + 1900,
                 result.tm_mon + 1,
                 result.tm_mday,
                 result.tm_hour,
                 result.tm_min,
                 result.tm_sec);
    }

    return buf;
}
  1. 函数定义

    实现了
    • Timestamp

      类中声明的函数:

    • std::string Timestamp::toString() const 返回时间的字符串表示形式。

    • 静态函数 Timestamp Timestamp::now() 用于获取当前时间;

    • 参数构造函数 Timestamp::Timestamp(int64_t microSecondsSinceEpoch)

    • 默认构造函数 Timestamp::Timestamp()

  2. 时间格式化

    Timestamp::toString() 函数将时间戳格式化为字符串形式,使用 localtime_rsnprintf 函数进行格式化。这段代码是一个 Timestamp 类中的成员函数 toString() 的实现,它用于将时间戳转换为字符串表示形式。在这个函数中:
    • 修改返回类型为 std::string,可以确保时间戳的字符串副本被正确地返回,而不是指向局部数组的悬挂指针。

    • char buf[128]:定义了一个大小为 128 的字符数组 buf,用于存储格式化后的时间戳字符串。

    • time_t timeValue = static_cast<time_t>(microSecondsSinceEpoch_ / 1000000):将类成员变量 microSecondsSinceEpoch_ 转换为秒级的时间戳,存储在 timeValue 中。
    • std::tm result:创建一个 std::tm 结构体,用于存储时间的分解信息。
    • if (localtime_r(&timeValue, &result) != nullptr):使用 localtime_r() 函数将时间戳 timeValue 转换为本地时间,并将结果存储在 result 中。localtime_r() 函数会将时间戳转换为本地时间,并填充 result 结构体,如果转换成功,则返回 result 结构体指针,否则返回 nullptr
    • snprintf(buf, 128, "%4d/%02d/%02d %02d:%02d:%02d", result.tm_year + 1900, result.tm_mon + 1, result.tm_mday, result.tm_hour, result.tm_min, result.tm_sec):使用 snprintf 函数将时间分解信息按照指定的格式写入 buf 数组中。这里的格式字符串 "%4d/%02d/%02d %02d:%02d:%02d" 将年月日时分秒格式化为字符串,并将其写入到 buf 中。
    • return buf:返回格式化后的时间戳字符串。但是需要注意的是,返回的是 buf 的指针,并且在函数返回时,buf 数组将超出其作用域,这可能导致未定义的行为。为了避免此问题,可以考虑返回一个 std::string 对象,而不是指向局部数组的指针。

 

总结

可以无所谓,不能无所获

                                        ——岩

GitHub代码链接:  GitHub - YanHeXi/muduo_cpp20
项目讲解视频连接:岩禾溪的个人空间-岩禾溪个人主页-哔哩哔哩视频