首页 > 编程语言 >easylogging++的那些事(四)源码分析(十四)其他工具类(二)

easylogging++的那些事(四)源码分析(十四)其他工具类(二)

时间:2022-12-09 21:25:30浏览次数:67  
标签:std return ++ easylogging char 源码 base const ELPP

目录

其他工具类一 中我们介绍了部分工具类,今天我们继续看看其他一些工具类。

NoCopy 类

    NoCopy 类主要用于防止对象被复制。

/// @detail When using this class simply inherit it privately
class NoCopy
{
protected:
    NoCopy(void) {}

private:
    NoCopy(const NoCopy &);
    NoCopy &operator=(const NoCopy &);
};

    禁用 COPY 语义并且只能被继承,不能直接构建 NoCopy 实例,源码当中用于私有继承,达到禁用 copy 语义的目的。

StaticClass 类

    StaticClass 类主要用于作为所有工具类的私有基类,工具类仅仅包含静态方法, 不能构建对象,正如其名。

/// @brief Internal helper class that makes all default constructors private.
///
/// @detail This prevents initializing class making it static unless an explicit constructor is declared.
/// When using this class simply inherit it privately
class StaticClass
{
private:
    StaticClass(void);
    StaticClass(const StaticClass &);
    StaticClass &operator=(const StaticClass &);
};

LevelHelper 类

    LevelHelper 类是 el::Level 的工具类。

/// @brief Static class that contains helper functions for el::Level
class LevelHelper : base::StaticClass
{
public:
    // 最小有效分层日志级别的常量定义
    /// @brief Represents minimum valid level. Useful when iterating through enum.
    static const base::type::EnumType kMinValid = static_cast<base::type::EnumType>(Level::Trace);

    // 最大有效分层日志级别的常量定义
    /// @brief Represents maximum valid level. This is used internally and you should not need it.
    static const base::type::EnumType kMaxValid = static_cast<base::type::EnumType>(Level::Info);

    // Level转EnumType
    /// @brief Casts level to int, useful for iterating through enum.
    static base::type::EnumType castToInt(Level level)
    {
        return static_cast<base::type::EnumType>(level);
    }

    // EnumType转Level
    /// @brief Casts int(ushort) to level, useful for iterating through enum.
    static Level castFromInt(base::type::EnumType l)
    {
        return static_cast<Level>(l);
    }

    // 返回分层日志级别的字符串表示形式
    /// @brief Converts level to associated const char*
    /// @return Upper case string based level.
    static const char *convertToString(Level level);

    // 将字符串形式的分层日志级别转为Level
    /// @brief Converts from levelStr to Level
    /// @param levelStr Upper case string based level.
    ///        Lower case is also valid but providing upper case is recommended.
    static Level convertFromString(const char *levelStr);

    // 从指定分层日志级别开始遍历,对于每个级别执行一些操作(fn),直到fn返回true为止。
    /// @brief Applies specified function to each level starting from startIndex
    /// @param startIndex initial value to start the iteration from. This is passed as pointer and
    ///        is left-shifted so this can be used inside function (fn) to represent current level.
    /// @param fn function to apply with each level. This bool represent whether or not to stop iterating through levels.
    static void forEachLevel(base::type::EnumType *startIndex, const std::function<bool(void)> &fn);
};

// 返回分层日志级别的字符串表示形式
const char *LevelHelper::convertToString(Level level)
{
    // Do not use switch over strongly typed enums because Intel C++ compilers dont support them yet.
    if (level == Level::Global)
        return "GLOBAL";
    if (level == Level::Debug)
        return "DEBUG";
    if (level == Level::Info)
        return "INFO";
    if (level == Level::Warning)
        return "WARNING";
    if (level == Level::Error)
        return "ERROR";
    if (level == Level::Fatal)
        return "FATAL";
    if (level == Level::Verbose)
        return "VERBOSE";
    if (level == Level::Trace)
        return "TRACE";
    return "UNKNOWN";
}

struct StringToLevelItem
{
    const char *levelString;
    Level level;
};

static struct StringToLevelItem stringToLevelMap[] = {
    {"global", Level::Global},
    {"debug", Level::Debug},
    {"info", Level::Info},
    {"warning", Level::Warning},
    {"error", Level::Error},
    {"fatal", Level::Fatal},
    {"verbose", Level::Verbose},
    {"trace", Level::Trace}};

// 将字符串形式的分层日志级别转为Level类型
Level LevelHelper::convertFromString(const char *levelStr)
{
    for (auto &item : stringToLevelMap)
    {
        if (base::utils::Str::cStringCaseEq(levelStr, item.levelString))
        {
            return item.level;
        }
    }
    return Level::Unknown;
}

// 从指定分层日志级别开始遍历,对于每个级别执行一些操作(fn),直到fn返回true为止。
void LevelHelper::forEachLevel(base::type::EnumType *startIndex, const std::function<bool(void)> &fn)
{
    base::type::EnumType lIndexMax = LevelHelper::kMaxValid;
    do
    {
        if (fn())
        {
            break;
        }
        *startIndex = static_cast<base::type::EnumType>(*startIndex << 1);
    } while (*startIndex <= lIndexMax);
}

ConfigurationTypeHelper 类

    ConfigurationTypeHelper 类是 el::ConfigurationType 的工具类。

/// @brief Static class that contains helper functions for el::ConfigurationType
class ConfigurationTypeHelper : base::StaticClass
{
public:
    // 最小有效ConfigurationType的常量定义
    /// @brief Represents minimum valid configuration type. Useful when iterating through enum.
    static const base::type::EnumType kMinValid = static_cast<base::type::EnumType>(ConfigurationType::Enabled);

    // 最大有效ConfigurationType的常量定义
    /// @brief Represents maximum valid configuration type. This is used internally and you should not need it.
    static const base::type::EnumType kMaxValid = static_cast<base::type::EnumType>(ConfigurationType::MaxLogFileSize);

    // ConfigurationType转EnumType
    /// @brief Casts configuration type to int, useful for iterating through enum.
    static base::type::EnumType castToInt(ConfigurationType configurationType)
    {
        return static_cast<base::type::EnumType>(configurationType);
    }
    // EnumType转ConfigurationType
    /// @brief Casts int(ushort) to configuration type, useful for iterating through enum.
    static ConfigurationType castFromInt(base::type::EnumType c)
    {
        return static_cast<ConfigurationType>(c);
    }
    /// @brief Converts configuration type to associated const char*
    /// @returns Upper case string based configuration type.
    static const char *convertToString(ConfigurationType configurationType);

    /// @brief Converts from configStr to ConfigurationType
    /// @param configStr Upper case string based configuration type.
    ///        Lower case is also valid but providing upper case is recommended.
    static ConfigurationType convertFromString(const char *configStr);
    /// @brief Applies specified function to each configuration type starting from startIndex
    /// @param startIndex initial value to start the iteration from. This is passed by pointer and is left-shifted
    ///        so this can be used inside function (fn) to represent current configuration type.
    /// @param fn function to apply with each configuration type.
    ///        This bool represent whether or not to stop iterating through configurations.
    static inline void forEachConfigType(base::type::EnumType *startIndex, const std::function<bool(void)> &fn);
};

const char *ConfigurationTypeHelper::convertToString(ConfigurationType configurationType)
{
    // Do not use switch over strongly typed enums because Intel C++ compilers dont support them yet.
    if (configurationType == ConfigurationType::Enabled)
        return "ENABLED";
    if (configurationType == ConfigurationType::Filename)
        return "FILENAME";
    if (configurationType == ConfigurationType::Format)
        return "FORMAT";
    if (configurationType == ConfigurationType::ToFile)
        return "TO_FILE";
    if (configurationType == ConfigurationType::ToStandardOutput)
        return "TO_STANDARD_OUTPUT";
    if (configurationType == ConfigurationType::SubsecondPrecision)
        return "SUBSECOND_PRECISION";
    if (configurationType == ConfigurationType::PerformanceTracking)
        return "PERFORMANCE_TRACKING";
    if (configurationType == ConfigurationType::MaxLogFileSize)
        return "MAX_LOG_FILE_SIZE";
    if (configurationType == ConfigurationType::LogFlushThreshold)
        return "LOG_FLUSH_THRESHOLD";
    return "UNKNOWN";
}

struct ConfigurationStringToTypeItem
{
    const char *configString;
    ConfigurationType configType;
};

static struct ConfigurationStringToTypeItem configStringToTypeMap[] = {
    {"enabled", ConfigurationType::Enabled},
    {"to_file", ConfigurationType::ToFile},
    {"to_standard_output", ConfigurationType::ToStandardOutput},
    {"format", ConfigurationType::Format},
    {"filename", ConfigurationType::Filename},
    {"subsecond_precision", ConfigurationType::SubsecondPrecision},
    {"milliseconds_width", ConfigurationType::MillisecondsWidth},
    {"performance_tracking", ConfigurationType::PerformanceTracking},
    {"max_log_file_size", ConfigurationType::MaxLogFileSize},
    {"log_flush_threshold", ConfigurationType::LogFlushThreshold},
};

ConfigurationType ConfigurationTypeHelper::convertFromString(const char *configStr)
{
    for (auto &item : configStringToTypeMap)
    {
        if (base::utils::Str::cStringCaseEq(configStr, item.configString))
        {
            return item.configType;
        }
    }
    return ConfigurationType::Unknown;
}

// 从指定ConfigurationType开始遍历,对于每个ConfigurationType执行一些操作的接口(fn),直到fn返回true为止。
void ConfigurationTypeHelper::forEachConfigType(base::type::EnumType *startIndex, const std::function<bool(void)> &fn)
{
    base::type::EnumType cIndexMax = ConfigurationTypeHelper::kMaxValid;
    do
    {
        if (fn())
        {
            break;
        }
        *startIndex = static_cast<base::type::EnumType>(*startIndex << 1);
    } while (*startIndex <= cIndexMax);
}

safeDelete 模板接口

    safeDelete 函数模板用于安全释放指针

/// @brief Deletes memory safely and points to null
template <typename T>
static
    typename std::enable_if<std::is_pointer<T *>::value, void>::type
    safeDelete(T *&pointer)
{
    if (pointer == nullptr)
        return;
    delete pointer;
    pointer = nullptr;
}

    运用了 SFINAE 模板技巧, enable_if 模板可以用它来选择性地启用某个函数的重载。
    如果 T*是指针的话,最终才会实例化这个函数模板,此时 typename std::enable_if<std::is_pointer<T*>::value, void >::type 的值为 void。
    不清楚的同学,可以查看 C++标准库的文档,这里就不多介绍了。

位运算

/// @brief Bitwise operations for C++11 strong enum class. This casts e into Flag_T and returns value after bitwise operation
/// Use these function as <pre>flag = bitwise::Or<MyEnum>(MyEnum::val1, flag);</pre>
namespace bitwise
{
    template <typename Enum>
    static inline base::type::EnumType And(Enum e, base::type::EnumType flag)
    {
        return static_cast<base::type::EnumType>(flag) & static_cast<base::type::EnumType>(e);
    }
    template <typename Enum>
    static inline base::type::EnumType Not(Enum e, base::type::EnumType flag)
    {
        return static_cast<base::type::EnumType>(flag) & ~(static_cast<base::type::EnumType>(e));
    }
    template <typename Enum>
    static inline base::type::EnumType Or(Enum e, base::type::EnumType flag)
    {
        return static_cast<base::type::EnumType>(flag) | static_cast<base::type::EnumType>(e);
    }
} // namespace bitwise
template <typename Enum>
static inline void addFlag(Enum e, base::type::EnumType *flag)
{
    *flag = base::utils::bitwise::Or<Enum>(e, *flag);
}
template <typename Enum>
static inline void removeFlag(Enum e, base::type::EnumType *flag)
{
    *flag = base::utils::bitwise::Not<Enum>(e, *flag);
}
template <typename Enum>
static inline bool hasFlag(Enum e, base::type::EnumType flag)
{
    return base::utils::bitwise::And<Enum>(e, flag) > 0x0;
}

File 类

    File 类是跨平台文件操作类

class File : base::StaticClass
{
public:
    // 根据指定的文件名打开一个新的文件流
    /// @brief Creates new out file stream for specified filename.
    /// @return Pointer to newly created fstream or nullptr
    static base::type::fstream_t *newFileStream(const std::string &filename);

    // 获取文件的大小
    /// @brief Gets size of file provided in stream
    static std::size_t getSizeOfFile(base::type::fstream_t *fs);

    // 指定路径对应的文件是否存在
    /// @brief Determines whether or not provided path exist in current file system
    static bool pathExists(const char *path, bool considerFile = false);

    // 递归创建文件目录
    /// @brief Creates specified path on file system
    /// @param path Path to create.
    static bool createPath(const std::string &path);

    // 获取文件的目录路径
    /// @brief Extracts path of filename with leading slash
    static std::string extractPathFromFilename(const std::string &fullPath, const char *separator = base::consts::kFilePathSeparator);

    // 调整文件名(仅仅只有文件名,不带路径),文件名过长超过缓冲区的大小时,文件名调整为以..开头+文件名剩余部分使其能够放入指定大小的缓冲区
    /// @brief builds stripped filename and puts it in buff
    static void buildStrippedFilename(const char *filename, char buff[], std::size_t limit = base::consts::kSourceFilenameMaxLength);

    // 和buildStrippedFilename功能一样,区别是参数为全路径文件名
    /// @brief builds base filename and puts it in buff
    static void buildBaseFilename(const std::string &fullPath, char buff[],
                                  std::size_t limit = base::consts::kSourceFilenameMaxLength,
                                  const char *separator = base::consts::kFilePathSeparator);
};


// 根据指定的文件名打开一个新的文件流
base::type::fstream_t *File::newFileStream(const std::string &filename)
{
    base::type::fstream_t *fs = new base::type::fstream_t(filename.c_str(),
                                                          base::type::fstream_t::out
#if !defined(ELPP_FRESH_LOG_FILE)
                                                              | base::type::fstream_t::app
#endif
    );
#if defined(ELPP_UNICODE)
    std::locale elppUnicodeLocale("");
#if ELPP_OS_WINDOWS
    std::locale elppUnicodeLocaleWindows(elppUnicodeLocale, new std::codecvt_utf8_utf16<wchar_t>);
    elppUnicodeLocale = elppUnicodeLocaleWindows;
#endif // ELPP_OS_WINDOWS
    fs->imbue(elppUnicodeLocale);
#endif // defined(ELPP_UNICODE)
    if (fs->is_open())
    {
        fs->flush();
    }
    else
    {
        base::utils::safeDelete(fs);
        ELPP_INTERNAL_ERROR("Bad file [" << filename << "]", true);
    }
    return fs;
}

// 获取文件的大小
std::size_t File::getSizeOfFile(base::type::fstream_t *fs)
{
    if (fs == nullptr)
    {
        return 0;
    }
    // Since the file stream is appended to or truncated, the current
    // offset is the file size.
    std::size_t size = static_cast<std::size_t>(fs->tellg());
    return size;
}

// 指定路径对应的文件是否存在
bool File::pathExists(const char *path, bool considerFile)
{
    if (path == nullptr)
    {
        return false;
    }
#if ELPP_OS_UNIX
    ELPP_UNUSED(considerFile);
    struct stat st;
    return (stat(path, &st) == 0);
#elif ELPP_OS_WINDOWS
    DWORD fileType = GetFileAttributesA(path);
    if (fileType == INVALID_FILE_ATTRIBUTES)
    {
        return false;
    }
    return considerFile ? true : ((fileType & FILE_ATTRIBUTE_DIRECTORY) == 0 ? false : true);
#endif // ELPP_OS_UNIX
}

// 递归创建文件目录
bool File::createPath(const std::string &path)
{
    if (path.empty())
    {
        return false;
    }
    if (base::utils::File::pathExists(path.c_str()))
    {
        return true;
    }
    int status = -1;

    char *currPath = const_cast<char *>(path.c_str());
    std::string builtPath = std::string();
#if ELPP_OS_UNIX
    if (path[0] == '/')
    {
        builtPath = "/";
    }
    currPath = STRTOK(currPath, base::consts::kFilePathSeparator, 0);
#elif ELPP_OS_WINDOWS
    // Use secure functions API
    char *nextTok_ = nullptr;
    currPath = STRTOK(currPath, base::consts::kFilePathSeparator, &nextTok_);
    ELPP_UNUSED(nextTok_);
#endif // ELPP_OS_UNIX
    while (currPath != nullptr)
    {
        builtPath.append(currPath);
        builtPath.append(base::consts::kFilePathSeparator);
#if ELPP_OS_UNIX
        status = mkdir(builtPath.c_str(), ELPP_LOG_PERMS);
        currPath = STRTOK(nullptr, base::consts::kFilePathSeparator, 0);
#elif ELPP_OS_WINDOWS
        status = _mkdir(builtPath.c_str());
        currPath = STRTOK(nullptr, base::consts::kFilePathSeparator, &nextTok_);
#endif // ELPP_OS_UNIX
    }
    if (status == -1)
    {
        ELPP_INTERNAL_ERROR("Error while creating path [" << path << "]", true);
        return false;
    }
    return true;
}

// 获取文件的目录路径
std::string File::extractPathFromFilename(const std::string &fullPath, const char *separator)
{
    if ((fullPath == "") || (fullPath.find(separator) == std::string::npos))
    {
        return fullPath;
    }
    std::size_t lastSlashAt = fullPath.find_last_of(separator);
    if (lastSlashAt == 0)
    {
        return std::string(separator);
    }
    return fullPath.substr(0, lastSlashAt + 1);
}

// 调整文件名(仅仅只有文件名,不带路径),文件名过长超过缓冲区的大小时,文件名调整为以..开头+文件名剩余部分使其能够放入指定大小的缓冲区
void File::buildStrippedFilename(const char *filename, char buff[], std::size_t limit)
{
    std::size_t sizeOfFilename = strlen(filename);
    if (sizeOfFilename >= limit)
    {
        filename += (sizeOfFilename - limit);
        if (filename[0] != '.' && filename[1] != '.')
        {                  // prepend if not already
            filename += 3; // 3 = '..'
            STRCAT(buff, "..", limit);
        }
    }
    STRCAT(buff, filename, limit);
}

// 和buildStrippedFilename功能一样,区别是参数为全路径文件名
void File::buildBaseFilename(const std::string &fullPath, char buff[], std::size_t limit, const char *separator)
{
    const char *filename = fullPath.c_str();
    std::size_t lastSlashAt = fullPath.find_last_of(separator);
    filename += lastSlashAt ? lastSlashAt + 1 : 0;
    std::size_t sizeOfFilename = strlen(filename);
    if (sizeOfFilename >= limit)
    {
        filename += (sizeOfFilename - limit);
        if (filename[0] != '.' && filename[1] != '.')
        {                  // prepend if not already
            filename += 3; // 3 = '..'
            STRCAT(buff, "..", limit);
        }
    }
    STRCAT(buff, filename, limit);
}

Str 类

    Str 类是跨平台字符串操作工具类

/// @brief String utilities helper class used internally. You should not use it.
class Str : base::StaticClass
{
public:
    // 字符是否为数字
    /// @brief Checks if character is digit. Dont use libc implementation of it to prevent locale issues.
    static inline bool isDigit(char c)
    {
        return c >= '0' && c <= '9';
    }

    // 字符串匹配,仅仅支持*和?
    /// @brief Matches wildcards, '*' and '?' only supported.
    static bool wildCardMatch(const char *str, const char *pattern);

    // 去除字符串左边的空白字符
    static std::string &ltrim(std::string &str);
    // 去除字符串右边边的空白字符
    static std::string &rtrim(std::string &str);
    // 去除字符串左右两边的空白字符
    static std::string &trim(std::string &str);

    // 检查字符串是否以start子串开始
    /// @brief Determines whether or not str starts with specified string
    /// @param str String to check
    /// @param start String to check against
    /// @return Returns true if starts with specified string, false otherwise
    static bool startsWith(const std::string &str, const std::string &start);

    // 检查字符串是否以end子串结束
    /// @brief Determines whether or not str ends with specified string
    /// @param str String to check
    /// @param end String to check against
    /// @return Returns true if ends with specified string, false otherwise
    static bool endsWith(const std::string &str, const std::string &end);

    // 替换所有指定的字符
    /// @brief Replaces all instances of replaceWhat with 'replaceWith'. Original variable is changed for performance.
    /// @param [in,out] str String to replace from
    /// @param replaceWhat Character to replace
    /// @param replaceWith Character to replace with
    /// @return Modified version of str
    static std::string &replaceAll(std::string &str, char replaceWhat, char replaceWith);

    // 替换所有指定的字符串
    /// @brief Replaces all instances of 'replaceWhat' with 'replaceWith'. (String version) Replaces in place
    /// @param str String to replace from
    /// @param replaceWhat Character to replace
    /// @param replaceWith Character to replace with
    /// @return Modified (original) str
    static std::string &replaceAll(std::string &str, const std::string &replaceWhat,
                                   const std::string &replaceWith);

    // 替换指定的字符串
    // 功能介绍:如果目标子串的前一个字符是'%',就仅仅去掉前面的%,然后继续查找,反之,当遇到第一个前面不是‘%’的目标子串时,进行替换,然后替换结束
    static void replaceFirstWithEscape(base::type::string_t &str, const base::type::string_t &replaceWhat,
                                       const base::type::string_t &replaceWith);
#if defined(ELPP_UNICODE)
    // unicode版本 调用普通版本
    static void replaceFirstWithEscape(base::type::string_t &str, const base::type::string_t &replaceWhat,
                                       const std::string &replaceWith);
#endif // defined(ELPP_UNICODE)
    // 字符串转为大写
    /// @brief Converts string to uppercase
    /// @param str String to convert
    /// @return Uppercase string
    static std::string &toUpper(std::string &str);

    // 字符串相等比较(区分大小写)
    /// @brief Compares cstring equality - uses strcmp
    static bool cStringEq(const char *s1, const char *s2);

    // 字符串相等比较(不区分大小写)
    /// @brief Compares cstring equality (case-insensitive) - uses toupper(char)
    /// Dont use strcasecmp because of CRT (VC++)
    static bool cStringCaseEq(const char *s1, const char *s2);

    // 判断字符串当中是否含有某个字符
    /// @brief Returns true if c exist in str
    static bool contains(const char *str, char c);

    // 就是用于将数字转字符,推荐做法是使用printf函数家族,也会不存在性能问题。
    static char *convertAndAddToBuff(std::size_t n, int len, char *buf, const char *bufLim, bool zeroPadded = true);
    // 将字符串的内容添加到某个缓冲区中
    static char *addToBuff(const char *str, char *buf, const char *bufLim);
    // 缓冲区清空
    static char *clearBuff(char buff[], std::size_t lim);

    // wchar*转为char*
    /// @brief Converts wchar* to char*
    ///        NOTE: Need to free return value after use!
    static char *wcharPtrToCharPtr(const wchar_t *line);
};

// 字符串匹配,仅仅支持*和?
bool Str::wildCardMatch(const char *str, const char *pattern)
{
    while (*pattern)
    {
        switch (*pattern)
        {
        case '?':
            if (!*str)
                return false;
            ++str;
            ++pattern;
            break;
        case '*':
            if (wildCardMatch(str, pattern + 1))
                return true;
            if (*str && wildCardMatch(str + 1, pattern))
                return true;
            return false;
        default:
            if (*str++ != *pattern++)
                return false;
            break;
        }
    }
    return !*str && !*pattern;
}

// 去除字符串左边的空白字符
std::string &Str::ltrim(std::string &str)
{
    str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](char c)
                                        { return !std::isspace(c); }));
    return str;
}
// 去除字符串右边边的空白字符
std::string &Str::rtrim(std::string &str)
{
    str.erase(std::find_if(str.rbegin(), str.rend(), [](char c)
                           { return !std::isspace(c); })
                  .base(),
              str.end());
    return str;
}
// 去除字符串左右两边的空白字符
std::string &Str::trim(std::string &str)
{
    return ltrim(rtrim(str));
}
// 检查字符串是否以start子串开始
bool Str::startsWith(const std::string &str, const std::string &start)
{
    return (str.length() >= start.length()) && (str.compare(0, start.length(), start) == 0);
}
// 检查字符串是否以end子串结束
bool Str::endsWith(const std::string &str, const std::string &end)
{
    return (str.length() >= end.length()) && (str.compare(str.length() - end.length(), end.length(), end) == 0);
}
// 替换所有指定的字符
std::string &Str::replaceAll(std::string &str, char replaceWhat, char replaceWith)
{
    std::replace(str.begin(), str.end(), replaceWhat, replaceWith);
    return str;
}
// 替换所有指定的字符串
std::string &Str::replaceAll(std::string &str, const std::string &replaceWhat,
                             const std::string &replaceWith)
{
    if (replaceWhat == replaceWith)
        return str;
    std::size_t foundAt = std::string::npos;
    while ((foundAt = str.find(replaceWhat, foundAt + 1)) != std::string::npos)
    {
        str.replace(foundAt, replaceWhat.length(), replaceWith);
    }
    return str;
}

// 替换指定的字符串
// 功能介绍:如果目标子串的前一个字符是'%',就仅仅去掉前面的%,然后继续查找,反之,当遇到第一个前面不是‘%’的目标子串时,进行替换,然后替换结束
void Str::replaceFirstWithEscape(base::type::string_t &str, const base::type::string_t &replaceWhat,
                                 const base::type::string_t &replaceWith)
{
    std::size_t foundAt = base::type::string_t::npos;
    while ((foundAt = str.find(replaceWhat, foundAt + 1)) != base::type::string_t::npos)
    {
        if (foundAt > 0 && str[foundAt - 1] == base::consts::kFormatSpecifierChar)
        {
            str.erase(foundAt - 1, 1);
            ++foundAt;
        }
        else
        {
            str.replace(foundAt, replaceWhat.length(), replaceWith);
            return;
        }
    }
}
#if defined(ELPP_UNICODE)
// unicode版本 调用普通版本
void Str::replaceFirstWithEscape(base::type::string_t &str, const base::type::string_t &replaceWhat,
                                 const std::string &replaceWith)
{
    replaceFirstWithEscape(str, replaceWhat, base::type::string_t(replaceWith.begin(), replaceWith.end()));
}
#endif // defined(ELPP_UNICODE)

// 字符串转为大写
std::string &Str::toUpper(std::string &str)
{
    std::transform(str.begin(), str.end(), str.begin(),
                   [](char c)
                   {
                       return static_cast<char>(::toupper(c));
                   });
    return str;
}

// 字符串相等比较(区分大小写)
bool Str::cStringEq(const char *s1, const char *s2)
{
    if (s1 == nullptr && s2 == nullptr)
        return true;
    if (s1 == nullptr || s2 == nullptr)
        return false;
    return strcmp(s1, s2) == 0;
}

// 字符串相等比较(不区分大小写)
bool Str::cStringCaseEq(const char *s1, const char *s2)
{
    if (s1 == nullptr && s2 == nullptr)
        return true;
    if (s1 == nullptr || s2 == nullptr)
        return false;

    // With thanks to cygwin for this code
    int d = 0;

    while (true)
    {
        const int c1 = toupper(*s1++);
        const int c2 = toupper(*s2++);

        if (((d = c1 - c2) != 0) || (c2 == '\0'))
        {
            break;
        }
    }

    return d == 0;
}

// 判断字符串当中是否含有某个字符
bool Str::contains(const char *str, char c)
{
    for (; *str; ++str)
    {
        if (*str == c)
            return true;
    }
    return false;
}

// 就是用于将数字转字符,推荐做法是使用printf函数家族,也不存在性能问题。
char *Str::convertAndAddToBuff(std::size_t n, int len, char *buf, const char *bufLim, bool zeroPadded)
{
    char localBuff[10] = "";
    char *p = localBuff + sizeof(localBuff) - 2;
    if (n > 0)
    {
        for (; n > 0 && p > localBuff && len > 0; n /= 10, --len)
            *--p = static_cast<char>(n % 10 + '0');
    }
    else
    {
        *--p = '0';
        --len;
    }
    if (zeroPadded)
        while (p > localBuff && len-- > 0)
            *--p = static_cast<char>('0');
    return addToBuff(p, buf, bufLim);
}

// 将字符串的内容添加到某个缓冲区中
char *Str::addToBuff(const char *str, char *buf, const char *bufLim)
{
    while ((buf < bufLim) && ((*buf = *str++) != '\0'))
        ++buf;
    return buf;
}

// 缓冲区清空
char *Str::clearBuff(char buff[], std::size_t lim)
{
    STRCPY(buff, "", lim);
    ELPP_UNUSED(lim); // For *nix we dont have anything using lim in above STRCPY macro
    return buff;
}

// wchar*转为char*
/// @brief Converts wchar* to char*
///        NOTE: Need to free return value after use!
char *Str::wcharPtrToCharPtr(const wchar_t *line)
{
    std::size_t len_ = wcslen(line) + 1;
    char *buff_ = static_cast<char *>(malloc(len_ + 1));
#if ELPP_OS_UNIX || (ELPP_OS_WINDOWS && !ELPP_CRT_DBG_WARNINGS)
    std::wcstombs(buff_, line, len_);
#elif ELPP_OS_WINDOWS
    std::size_t convCount_ = 0;
    mbstate_t mbState_;
    ::memset(static_cast<void *>(&mbState_), 0, sizeof(mbState_));
    wcsrtombs_s(&convCount_, buff_, len_, &line, len_, &mbState_);
#endif // ELPP_OS_UNIX || (ELPP_OS_WINDOWS && !ELPP_CRT_DBG_WARNINGS)
    return buff_;
}

DateTime 类

    DateTime 类是日期时间工具类

/// @brief Contains utilities for cross-platform date/time. This class make use of el::base::utils::Str
class DateTime : base::StaticClass
{
public:
    // 获取当前时间(跨平台)
    /// @brief Cross platform gettimeofday for Windows and unix platform. This can be used to determine current microsecond.
    ///
    /// @detail For unix system it uses gettimeofday(timeval*, timezone*) and for Windows, a separate implementation is provided
    /// @param [in,out] tv Pointer that gets updated
    static void gettimeofday(struct timeval *tv);

    // 根据指定的时间日期格式将时间转为为对应的字符串形式
    /// @brief Gets current date and time with a subsecond part.
    /// @param format User provided date/time format
    /// @param ssPrec A pointer to base::SubsecondPrecision from configuration (non-null)
    /// @returns string based date time in specified format.
    static std::string getDateTime(const char *format, const base::SubsecondPrecision *ssPrec);

    // timeval结构体转字符串
    /// @brief Converts timeval (struct from ctime) to string using specified format and subsecond precision
    static std::string timevalToString(struct timeval tval, const char *format,
                                       const el::base::SubsecondPrecision *ssPrec);

    // 将指定的时间戳转化为指定时间单位的字符串形式
    /// @brief Formats time to get unit accordingly, units like second if > 1000 or minutes if > 60000 etc
    static base::type::string_t formatTime(unsigned long long time, base::TimestampUnit timestampUnit);

    // 求timeval结构体之间的时间差(以毫秒或者微秒为单位)
    /// @brief Gets time difference in milli/micro second depending on timestampUnit
    static unsigned long long getTimeDifference(const struct timeval &endTime, const struct timeval &startTime,
                                                base::TimestampUnit timestampUnit);

    // 获取当前时间的tm结构体
    static struct ::tm *buildTimeInfo(struct timeval *currTime, struct ::tm *timeInfo);

private:
    // 将日期时间按照指定的格式转为为对应的字符串形式(用于timevalToString)
    static char *parseFormat(char *buf, std::size_t bufSz, const char *format, const struct tm *tInfo,
                             std::size_t msec, const base::SubsecondPrecision *ssPrec);
};

// 获取当前时间(跨平台)
void DateTime::gettimeofday(struct timeval *tv)
{
#if ELPP_OS_WINDOWS
    if (tv != nullptr)
    {
#if ELPP_COMPILER_MSVC || defined(_MSC_EXTENSIONS)
        const unsigned __int64 delta_ = 11644473600000000Ui64;
#else
        const unsigned __int64 delta_ = 11644473600000000ULL;
#endif // ELPP_COMPILER_MSVC || defined(_MSC_EXTENSIONS)
        const double secOffSet = 0.000001;
        const unsigned long usecOffSet = 1000000;
        FILETIME fileTime;
        GetSystemTimeAsFileTime(&fileTime);
        unsigned __int64 present = 0;
        present |= fileTime.dwHighDateTime;
        present = present << 32;
        present |= fileTime.dwLowDateTime;
        present /= 10; // mic-sec
        // Subtract the difference
        present -= delta_;
        tv->tv_sec = static_cast<long>(present * secOffSet);
        tv->tv_usec = static_cast<long>(present % usecOffSet);
    }
#else
    ::gettimeofday(tv, nullptr);
#endif // ELPP_OS_WINDOWS
}

// 根据指定的时间日期格式将时间转为为对应的字符串形式
std::string DateTime::getDateTime(const char *format, const base::SubsecondPrecision *ssPrec)
{
    struct timeval currTime;
    gettimeofday(&currTime);
    return timevalToString(currTime, format, ssPrec);
}

// timeval结构体转字符串
std::string DateTime::timevalToString(struct timeval tval, const char *format,
                                      const el::base::SubsecondPrecision *ssPrec)
{
    struct ::tm timeInfo;
    buildTimeInfo(&tval, &timeInfo);
    const int kBuffSize = 30;
    char buff_[kBuffSize] = "";
    parseFormat(buff_, kBuffSize, format, &timeInfo, static_cast<std::size_t>(tval.tv_usec / ssPrec->m_offset),
                ssPrec);
    return std::string(buff_);
}

const struct
{
    double value;
    const base::type::char_t *unit;
} kTimeFormats[] = {
    {1000.0f, ELPP_LITERAL("us")},
    {1000.0f, ELPP_LITERAL("ms")},
    {60.0f, ELPP_LITERAL("seconds")},
    {60.0f, ELPP_LITERAL("minutes")},
    {24.0f, ELPP_LITERAL("hours")},
    {7.0f, ELPP_LITERAL("days")}};

// 将指定的时间戳转化为指定时间单位的字符串形式
base::type::string_t DateTime::formatTime(unsigned long long time, base::TimestampUnit timestampUnit)
{
    base::type::EnumType start = static_cast<base::type::EnumType>(timestampUnit);
    const base::type::char_t *unit = base::consts::kTimeFormats[start].unit;
    // 循环的这段逻辑没完全看明白,看注释大概知道是逐步找到最合适的那个时间单位,但和代码还没有完全对应上
    /// @brief Formats time to get unit accordingly, units like second if > 1000 or minutes if > 60000 etc
    for (base::type::EnumType i = start; i < base::consts::kTimeFormatsCount - 1; ++i)
    {
        // 小于一个时间单位的值
        if (time <= base::consts::kTimeFormats[i].value)
        {
            break;
        }
        // 以1000为一个基准时间单位("us"或者"ms"),但不明白的是这里为什么要time / 1000.0f < 1.9f用这个作为条件,有清楚的同学可以在留言区说一下,谢谢。
        if (base::consts::kTimeFormats[i].value == 1000.0f && time / 1000.0f < 1.9f)
        {
            break;
        }
        time /= static_cast<decltype(time)>(base::consts::kTimeFormats[i].value);
        unit = base::consts::kTimeFormats[i + 1].unit;
    }
    base::type::stringstream_t ss;
    ss << time << " " << unit;
    return ss.str();
}

// 求timeval结构体之间的时间差(以毫秒或者微秒为单位)
unsigned long long DateTime::getTimeDifference(const struct timeval &endTime, const struct timeval &startTime,
                                               base::TimestampUnit timestampUnit)
{
    if (timestampUnit == base::TimestampUnit::Microsecond)
    {
        return static_cast<unsigned long long>(static_cast<unsigned long long>(1000000 * endTime.tv_sec + endTime.tv_usec) -
                                               static_cast<unsigned long long>(1000000 * startTime.tv_sec + startTime.tv_usec));
    }
    // milliseconds
    auto conv = [](const struct timeval &tim)
    {
        return static_cast<unsigned long long>((tim.tv_sec * 1000) + (tim.tv_usec / 1000));
    };
    return static_cast<unsigned long long>(conv(endTime) - conv(startTime));
}

// 获取当前时间的tm结构体
struct ::tm *DateTime::buildTimeInfo(struct timeval *currTime, struct ::tm *timeInfo)
{
#if ELPP_OS_UNIX
    time_t rawTime = currTime->tv_sec;
    ::elpptime_r(&rawTime, timeInfo);
    return timeInfo;
#else
#if ELPP_COMPILER_MSVC
    ELPP_UNUSED(currTime);
    time_t t;
#if defined(_USE_32BIT_TIME_T)
    _time32(&t);
#else
    _time64(&t);
#endif
    elpptime_s(timeInfo, &t);
    return timeInfo;
#else
    // For any other compilers that don't have CRT warnings issue e.g, MinGW or TDM GCC- we use different method
    time_t rawTime = currTime->tv_sec;
    struct tm *tmInf = elpptime(&rawTime);
    *timeInfo = *tmInf;
    return timeInfo;
#endif // ELPP_COMPILER_MSVC
#endif // ELPP_OS_UNIX
}

// 将日期时间按照指定的格式转为为对应的字符串形式(用于timevalToString)
char *DateTime::parseFormat(char *buf, std::size_t bufSz, const char *format, const struct tm *tInfo,
                            std::size_t msec, const base::SubsecondPrecision *ssPrec)
{
    const char *bufLim = buf + bufSz;
    for (; *format; ++format)
    {
        if (*format == base::consts::kFormatSpecifierChar)
        {
            switch (*++format)
            {
            case base::consts::kFormatSpecifierChar: // Escape
                break;
            case '\0': // End
                --format;
                break;
            case 'd': // Day
                buf = base::utils::Str::convertAndAddToBuff(tInfo->tm_mday, 2, buf, bufLim);
                continue;
            case 'a': // Day of week (short)
                buf = base::utils::Str::addToBuff(base::consts::kDaysAbbrev[tInfo->tm_wday], buf, bufLim);
                continue;
            case 'A': // Day of week (long)
                buf = base::utils::Str::addToBuff(base::consts::kDays[tInfo->tm_wday], buf, bufLim);
                continue;
            case 'M': // month
                buf = base::utils::Str::convertAndAddToBuff(tInfo->tm_mon + 1, 2, buf, bufLim);
                continue;
            case 'b': // month (short)
                buf = base::utils::Str::addToBuff(base::consts::kMonthsAbbrev[tInfo->tm_mon], buf, bufLim);
                continue;
            case 'B': // month (long)
                buf = base::utils::Str::addToBuff(base::consts::kMonths[tInfo->tm_mon], buf, bufLim);
                continue;
            case 'y': // year (two digits)
                buf = base::utils::Str::convertAndAddToBuff(tInfo->tm_year + base::consts::kYearBase, 2, buf, bufLim);
                continue;
            case 'Y': // year (four digits)
                buf = base::utils::Str::convertAndAddToBuff(tInfo->tm_year + base::consts::kYearBase, 4, buf, bufLim);
                continue;
            case 'h': // hour (12-hour)
                buf = base::utils::Str::convertAndAddToBuff(tInfo->tm_hour % 12, 2, buf, bufLim);
                continue;
            case 'H': // hour (24-hour)
                buf = base::utils::Str::convertAndAddToBuff(tInfo->tm_hour, 2, buf, bufLim);
                continue;
            case 'm': // minute
                buf = base::utils::Str::convertAndAddToBuff(tInfo->tm_min, 2, buf, bufLim);
                continue;
            case 's': // second
                buf = base::utils::Str::convertAndAddToBuff(tInfo->tm_sec, 2, buf, bufLim);
                continue;
            case 'z': // subsecond part
            case 'g':
                buf = base::utils::Str::convertAndAddToBuff(msec, ssPrec->m_width, buf, bufLim);
                continue;
            case 'F': // AM/PM
                buf = base::utils::Str::addToBuff((tInfo->tm_hour >= 12) ? base::consts::kPm : base::consts::kAm, buf, bufLim);
                continue;
            default:
                continue;
            }
        }
        if (buf == bufLim)
            break;
        *buf++ = *format;
    }
    return buf;
}

OS 类

    OS 类是操作系统相关工具类

/// @brief Operating System helper static class used internally. You should not use it.
class OS : base::StaticClass
{
public:
#if ELPP_OS_WINDOWS
    // 获取windows系统环境变量的值
    /// @brief Gets environment variables for Windows based OS.
    ///        We are not using <code>getenv(const char*)</code> because of CRT deprecation
    /// @param varname Variable name to get environment variable value for
    /// @return If variable exist the value of it otherwise nullptr
    static const char *getWindowsEnvironmentVariable(const char *varname);
#endif // ELPP_OS_WINDOWS

// 安卓系统相关
#if ELPP_OS_ANDROID
    /// @brief Reads android property value
    static std::string getProperty(const char *prop);

    /// @brief Reads android device name
    static std::string getDeviceName(void);
#endif // ELPP_OS_ANDROID

    // 获取控制台命令执行结果(仅支持类unix系统,其他返回空字符串)
    /// @brief Runs command on terminal and returns the output.
    ///
    /// @detail This is applicable only on unix based systems, for all other OS, an empty string is returned.
    /// @param command Bash command
    /// @return Result of bash output or empty string if no result found.
    static const std::string getBashOutput(const char *command);

    // 获取环境变量的值(跨平台)
    /// @brief Gets environment variable. This is cross-platform and CRT safe (for VC++)
    /// @param variableName Environment variable name
    /// @param defaultVal If no environment variable or value found the value to return by default
    /// @param alternativeBashCommand If environment variable not found what would be alternative bash command
    ///        in order to look for value user is looking for. E.g, for 'user' alternative command will 'whoami'
    static std::string getEnvironmentVariable(const char *variableName, const char *defaultVal, const char *alternativeBashCommand = nullptr);

    // 获取当前用户名(跨平台)
    /// @brief Gets current username.
    static std::string currentUser(void);

    // 获取当前主机名(跨平台)
    /// @brief Gets current host name or computer name.
    ///
    /// @detail For android systems this is device name with its manufacturer and model separated by hyphen
    static std::string currentHost(void);

    // 是否终端支持彩色显示(跨平台)
    /// @brief Whether or not terminal supports colors
    static bool termSupportsColor(void);
};

#if ELPP_OS_WINDOWS
// 获取windows系统环境变量的值
/// @brief Gets environment variables for Windows based OS.
///        We are not using <code>getenv(const char*)</code> because of CRT deprecation
/// @param varname Variable name to get environment variable value for
/// @return If variable exist the value of it otherwise nullptr
const char *OS::getWindowsEnvironmentVariable(const char *varname)
{
    const DWORD bufferLen = 50;
    static char buffer[bufferLen];
    if (GetEnvironmentVariableA(varname, buffer, bufferLen))
    {
        return buffer;
    }
    return nullptr;
}
#endif // ELPP_OS_WINDOWS

// 安卓系统相关
#if ELPP_OS_ANDROID
std::string OS::getProperty(const char *prop)
{
    char propVal[PROP_VALUE_MAX + 1];
    int ret = __system_property_get(prop, propVal);
    return ret == 0 ? std::string() : std::string(propVal);
}

std::string OS::getDeviceName(void)
{
    std::stringstream ss;
    std::string manufacturer = getProperty("ro.product.manufacturer");
    std::string model = getProperty("ro.product.model");
    if (manufacturer.empty() || model.empty())
    {
        return std::string();
    }
    ss << manufacturer << "-" << model;
    return ss.str();
}
#endif // ELPP_OS_ANDROID

// 获取控制台命令执行结果(仅支持类unix系统,其他返回空字符串)
const std::string OS::getBashOutput(const char *command)
{
#if (ELPP_OS_UNIX && !ELPP_OS_ANDROID && !ELPP_CYGWIN)
    if (command == nullptr)
    {
        return std::string();
    }
    FILE *proc = nullptr;
    if ((proc = popen(command, "r")) == nullptr)
    {
        ELPP_INTERNAL_ERROR("\nUnable to run command [" << command << "]", true);
        return std::string();
    }
    char hBuff[4096];
    if (fgets(hBuff, sizeof(hBuff), proc) != nullptr)
    {
        pclose(proc);
        const std::size_t buffLen = strlen(hBuff);
        if (buffLen > 0 && hBuff[buffLen - 1] == '\n')
        {
            hBuff[buffLen - 1] = '\0';
        }
        return std::string(hBuff);
    }
    else
    {
        pclose(proc);
    }
    return std::string();
#else
    ELPP_UNUSED(command);
    return std::string();
#endif // (ELPP_OS_UNIX && !ELPP_OS_ANDROID && !ELPP_CYGWIN)
}

// 获取环境变量的值(跨平台)
std::string OS::getEnvironmentVariable(const char *variableName, const char *defaultVal, const char *alternativeBashCommand)
{
#if ELPP_OS_UNIX
    const char *val = getenv(variableName);
#elif ELPP_OS_WINDOWS
    const char *val = getWindowsEnvironmentVariable(variableName);
#endif // ELPP_OS_UNIX
    if ((val == nullptr) || ((strcmp(val, "") == 0)))
    {
#if ELPP_OS_UNIX && defined(ELPP_FORCE_ENV_VAR_FROM_BASH)
        // Try harder on unix-based systems
        std::string valBash = base::utils::OS::getBashOutput(alternativeBashCommand);
        if (valBash.empty())
        {
            return std::string(defaultVal);
        }
        else
        {
            return valBash;
        }
#elif ELPP_OS_WINDOWS || ELPP_OS_UNIX
        ELPP_UNUSED(alternativeBashCommand);
        return std::string(defaultVal);
#endif // ELPP_OS_UNIX && defined(ELPP_FORCE_ENV_VAR_FROM_BASH)
    }
    return std::string(val);
}

// 获取当前用户名(跨平台)
std::string OS::currentUser(void)
{
#if ELPP_OS_UNIX && !ELPP_OS_ANDROID
    return getEnvironmentVariable("USER", base::consts::kUnknownUser, "whoami");
#elif ELPP_OS_WINDOWS
    return getEnvironmentVariable("USERNAME", base::consts::kUnknownUser);
#elif ELPP_OS_ANDROID
    ELPP_UNUSED(base::consts::kUnknownUser);
    return std::string("android");
#else
    return std::string();
#endif // ELPP_OS_UNIX && !ELPP_OS_ANDROID
}

// 获取当前主机名(跨平台)
std::string OS::currentHost(void)
{
#if ELPP_OS_UNIX && !ELPP_OS_ANDROID
    return getEnvironmentVariable("HOSTNAME", base::consts::kUnknownHost, "hostname");
#elif ELPP_OS_WINDOWS
    return getEnvironmentVariable("COMPUTERNAME", base::consts::kUnknownHost);
#elif ELPP_OS_ANDROID
    ELPP_UNUSED(base::consts::kUnknownHost);
    return getDeviceName();
#else
    return std::string();
#endif // ELPP_OS_UNIX && !ELPP_OS_ANDROID
}

// 是否终端支持彩色显示(跨平台)
bool OS::termSupportsColor(void)
{
    std::string term = getEnvironmentVariable("TERM", "");
    return term == "xterm" || term == "xterm-color" || term == "xterm-256color" || term == "screen" || term == "linux" || term == "cygwin" || term == "screen-256color";
}

    OS 类的相关接口都是调用操作系统底层相关的 API,这里就不多说了。

至此,工具类就差不多分析完了,最后还剩下线程安全相关的类没有介绍。下一篇我们来看看线程安全相关的一些类。

标签:std,return,++,easylogging,char,源码,base,const,ELPP
From: https://www.cnblogs.com/DesignLife/p/16970002.html

相关文章

  • Windows,C++编程创建窗口的过程详解
    MFC创建窗口一般要经历以下四个操作步骤:(1)   定义窗口类主要指定窗口类的一些基本且必须指定的特征,窗口类的特征主要是由WNDCLASS结构体来定义的,WNDCLASS的定义如下:type......
  • Metal 开发 | 使用 C++ 进行接口调用~~
    文章首发博客:https://glumes.com前两天在群里面看到大佬转发一篇文章:GettingstartedwithMetal-cpp。链接在此:​​https://developer.apple.com/metal/cpp/​​文章大意......
  • C++基础
    1.任何合法的C程序都是C++程序。2.c++是一门面向对象的语言。3.标准的C++由三个重要部分组成:核心语言,提供了所有构件块,包括变量、数据类型和常量,等等。C++标准库,提......
  • 安卓APP源码和设计报告——麻雀笔记
    OverridetheentrypointofanimageIntroducedinGitLabandGitLabRunner9.4.Readmoreaboutthe extendedconfigurationoptions.Beforeexplainingtheav......
  • 解决下载Android源码时遇到的 download error
    我下载和编译环境ubuntu12.0464位lenovothinkpadi32G最近要做android的浏览器插件要用到android的源码参与编译(不是指framework层源码)按照google的官网设......
  • 安卓APP源码和设计报告——智能垃圾桶
    OverridetheentrypointofanimageIntroducedinGitLabandGitLabRunner9.4.Readmoreaboutthe extendedconfigurationoptions.Beforeexplainingtheav......
  • 泰山众筹dapp项目开发原理分析(源码搭建)
    泰山众筹”是一种以卖货为主的一种电商商业模式。众筹模式是商城快速驱动用户自我裂变的一种促销活动。通过用户主动发起链接人脉,好友互助的模式,以更低的门槛参与并完成项......
  • C++ shared_ptr的实现
    比较完整的复现了STL中的shared_ptr中的功能,可以加载删除器,并且加入了互斥,可以在多线程中使用。#include<iostream>#include<mutex>usingnamespacestd;template......
  • [附源码]Python计算机毕业设计Django酒店物联网平台系统
    OverridetheentrypointofanimageIntroducedinGitLabandGitLabRunner9.4.Readmoreaboutthe extendedconfigurationoptions.Beforeexplainingtheav......
  • gin源码学习-中间件解析(3)
    目录1.gin中间件1.1全局中间件1.2组内中间件1.3单个路由中间件1.gin中间件中间件直接说源码优点枯燥,不妨从demo来以小窥大。packagemainimport( "log" "net/ht......