基于c++17的高性能日志库easylog介绍

在c++中使用日志库有很多选择,如spdlog,g3log,log4cxx,log4cplus,log4qt等。它们都好用且强大,就是有的有些有些重量级了,源码量来说。这里介绍一个性能极好且轻量级日志库easylog,来自阿里的雅兰亭库。仅简单的几个文件,使用时包含头文件即可。

easylog简介

easylog,阿里开源的轻量级高性能c++日志库,最低要求编译器支持c++17。属于阿里的雅兰亭库中集成的一个功能,代码量少,使用简单且性能强大。它使用了一些c++17以上的新功能特性如constexpr编译期优化,字符串视图类std::string_view,以及三方库ConcurrentQueue(线程安全的无锁队列),jkj::dragonbox(高效的浮点数到字符串的转换库),efvalue::meta_string字符串元编程,因此具有很高的性能。

GitHub - purecpp-org/easylog: a c++20 log lib

easylog实现

easylog的实现思路比较简单清晰。就是写入日志写入到队列里,不直接操作文件。开启一个线程一直从队列里取数据并写入文件。虽然代码量少,实现思路简单,但里面使用了很多c++的性能优化的新特性,值得学习和借鉴。

ConcurrentQueue

ConcurrentQueue是一种线程安全的队列数据结构,它允许多个线程同时对队列进行操作,而不需要额外的同步机制。它是并发编程中常用的数据结构之一。

moodycamel::ConcurrentQueue一个用C++11实现的多生产者、多消费者无锁队列。

仓库地址:

https://github.com/cameron314/concurrentqueue

优点如下:

1. 线程安全:ConcurrentQueue提供了内置的线程安全机制,可以在多个线程同时进行入队和出队操作,而无需手动添加额外的同步机制(如互斥锁或信号量)来保护共享资源。这简化了并发编程的复杂性。

2. 高效性能:ConcurrentQueue在并发环境下提供了较好的性能。它使用了一些高效的算法和数据结构,如无锁队列(lock-free queue)或细粒度锁(fine-grained locking),以减少竞争和提高并发性能。

3. 低延迟:由于ConcurrentQueue的设计目标是支持高并发和低延迟的场景,因此它通常具有较低的操作延迟。这对于需要快速响应和处理大量并发请求的应用程序非常有用。

4. 可扩展性:ConcurrentQueue可以在需要时轻松扩展到更多的线程,而不会出现性能瓶颈。它适用于高并发和高吞吐量的应用程序,可以根据需求进行水平扩展。

ConcurrentQueue是一种方便、高效、线程安全的队列数据结构,适用于并发编程场景,能够提供较好的性能和可扩展性。

dragonbox库

jkj::dragonbox是一个C++库,用于进行高效的浮点数到字符串的转换。它提供了一种快速且精确的方法,将浮点数表示为十进制字符串,适用于各种应用场景,如数字格式化、日志记录等。

仓库地址:

https://github.com/jk-jeon/dragonbox

jkj::dragonbox库的主要特点和用途:

1. 高效性能:jkj::dragonbox使用了一些高效的算法和技术,以实现快速的浮点数到字符串的转换。它在大多数情况下比标准库中的转换函数更快,并且具有可预测的性能。

2. 精确度:jkj::dragonbox库提供了精确的转换结果,能够保留浮点数的所有有效位数,并且在四舍五入时能够正确处理舍入误差。

3. 可移植性:jkj::dragonbox库是一个跨平台的C++库,可以在各种操作系统和编译器上使用。 4. 简单易用:jkj::dragonbox库的接口简单易用,只需包含相应的头文件,并调用相应的转换函数即可完成浮点数到字符串的转换。

jkj::dragonbox库提供了一个高效、精确和可移植的浮点数到字符串转换的解决方案,适用于需要高性能和精确度的应用场景。

constexpr特性

constexpr是C++11引入的关键字,用于声明一个编译时常量(compile-time constant)。它可以在编译时求值,并在编译时进行优化,提供了一种在编译阶段进行计算和初始化的能力。

1. 常量表达式:constexpr可以用于声明常量表达式,即在编译时就可以确定其值的表达式。这些表达式可以在编译时被求值,而不需要在运行时进行计算。

2. 编译时优化:使用constexpr声明的常量表达式可以在编译时进行优化,以提高程序的性能。编译器可以在编译时计算constexpr表达式的结果,并将其直接替换为结果值,而不需要在运行时进行计算。

3. 类型检查:constexpr还可以用于声明函数、构造函数、成员函数和类的成员变量。这些声明可以在编译时进行类型检查,以确保其满足constexpr的要求。

4. 数组大小:constexpr可以用于声明数组的大小,即在编译时确定数组的大小。这样可以在编译时进行静态检查,避免数组越界等问题。 使用constexpr可以提高代码的性能和可读性,同时允许在编译时进行更多的计算和优化。它在编译时求值的特性使得一些常量的计算可以在编译时完成,而不需要在运行时进行计算,从而提高了程序的效率。

meta_string特性

GET_STRING宏定义

用于在编译时生成一个包含文件名和行号信息的字符串前缀。它的作用是在日志输出等场景中,为每条日志消息添加包含文件名和行号的前缀,以便于定位日志消息的来源。

#define TO_STR(s) #s

#define GET_STRING(filename, line)                              \
  [] {                                                          \
    constexpr auto path = refvalue::meta_string{filename};      \
    constexpr size_t pos =                                      \
        path.rfind(std::filesystem::path::preferred_separator); \
    constexpr auto name = path.substr();               \
    constexpr auto prefix = name + ":" + TO_STR(line);          \
    return "[" + prefix + "] ";                                 \
  }()

其实可看做一个在编译时生成字符串字面量的元编程工具。它提供了一种在编译时创建字符串的能力,可以在编译时进行字符串相关的操作和计算。它定义了一个模板结构体,用于在编译期间处理字符串,并提供一些字符串操作的功能。

#pragma once

#include 
#include 
#include 
#if __has_include()
#include 
#include 
#include 
#endif
#include 
#include 

namespace refvalue {
template 
struct meta_string {
  std::array elements_;

  constexpr meta_string() noexcept : elements_{} {}

  constexpr meta_string(const char (&data)[N + 1]) noexcept {
    for (size_t i = 0; i < N + 1; i++) elements_[i] = data[i];
  }

#if __has_include()
  template 
  constexpr meta_string(std::span... data) noexcept
      : elements_{} {
    auto iter = elements_.begin();

    ((iter = std::copy(data.begin(), data.end(), iter)), ...);
  }
#endif

  template 
  constexpr meta_string(const meta_string&... data) noexcept : elements_{} {
    auto iter = elements_.begin();

    ((iter = std::copy(data.begin(), data.end(), iter)), ...);
  }

#if __has_include()
  template ... Ts>
  constexpr meta_string(Ts... chars) noexcept requires(sizeof...(Ts) == N)
      : elements_{chars...} {}
#endif

  constexpr char& operator[](std::size_t index) noexcept {
    return elements_[index];
  }

  constexpr const char& operator[](std::size_t index) const noexcept {
    return elements_[index];
  }

  constexpr operator std::string_view() const noexcept {
    return std::string_view{elements_.data(), size()};
  }

  constexpr bool empty() const noexcept { return size() == 0; }

  constexpr std::size_t size() const noexcept { return N; }

  constexpr char& front() noexcept { return elements_.front(); }

  constexpr const char& front() const noexcept { return elements_.front(); }

  constexpr char& back() noexcept { return elements_[size() - 1]; }

  constexpr const char& back() const noexcept { return elements_[size() - 1]; }

  constexpr auto begin() noexcept { return elements_.begin(); }

  constexpr auto begin() const noexcept { return elements_.begin(); }

  constexpr auto end() noexcept { return elements_.begin() + size(); }

  constexpr auto end() const noexcept { return elements_.begin() + size(); }

  constexpr char* data() noexcept { return elements_.data(); }

  constexpr const char* data() const noexcept { return elements_.data(); };

  constexpr const char* c_str() const noexcept { return elements_.data(); }

  constexpr bool contains(char c) const noexcept {
    return std::find(begin(), end(), c) != end();
  }

  constexpr bool contains(std::string_view str) const noexcept {
    return str.size() <= size()
               ? std::search(begin(), end(), str.begin(), str.end()) != end()
               : false;
  }

  static constexpr size_t substr_len(size_t pos, size_t count) {
    if (pos >= N) {
      return 0;
    }
    else if (count == std::string_view::npos || pos + count > N) {
      return N - pos;
    }
    else {
      return count;
    }
  }

  template 
  constexpr meta_string substr() const noexcept {
    constexpr size_t n = substr_len(pos, count);

    meta_string result;
    for (int i = 0; i < n; ++i) {
      result[i] = elements_[pos + i];
    }
    return result;
  }

  constexpr size_t rfind(char c) const noexcept {
    return std::string_view(*this).rfind(c);
  }

  constexpr size_t find(char c) const noexcept {
    return std::string_view(*this).find(c);
  }
};

template 
meta_string(const char (&)[N]) -> meta_string;

#if __has_include()
template 
meta_string(std::span...) -> meta_string<(Ns + ...)>;
#endif

template 
meta_string(const meta_string&...) -> meta_string<(Ns + ...)>;

#if __has_include()
template ... Ts>
meta_string(Ts...) -> meta_string;
#endif

#if __has_include()
template 
constexpr auto operator<=>(const meta_string& left,
                           const meta_string& right) noexcept {
  return static_cast(left).compare(
             static_cast(right)) <=> 0;
}
#endif

template 
constexpr bool operator==(const meta_string& left,
                          const meta_string& right) noexcept {
  return static_cast(left) ==
         static_cast(right);
}

template 
constexpr bool operator==(const meta_string& left,
                          const char (&right)[N]) noexcept {
  return static_cast(left) ==
         static_cast(meta_string{right});
}

template 
constexpr auto operator+(const meta_string& left,
                         const meta_string& right) noexcept {
  return meta_string{left, right};
}

template 
constexpr auto operator+(const meta_string& left,
                         const char (&right)[N]) noexcept {
  meta_string s;
  for (size_t i = 0; i < M; ++i) s[i] = left[i];
  for (size_t i = 0; i < N; ++i) s[M + i] = right[i];
  return s;
}

template 
constexpr auto operator+(const char (&left)[M],
                         const meta_string& right) noexcept {
  meta_string s;
  for (size_t i = 0; i < M - 1; ++i) s[i] = left[i];
  for (size_t i = 0; i < N; ++i) s[M + i - 1] = right[i];
  return s;
}

#if __has_include()
template 
struct split_of {
  static constexpr auto value = [] {
    constexpr std::string_view view{S};
    constexpr auto group_count = std::count_if(S.begin(), S.end(),
                                               [](char c) {
                                                 return Delim.contains(c);
                                               }) +
                                 1;
    std::array result{};

    auto iter = result.begin();

    for (std::size_t start_index = 0, end_index = view.find_first_of(Delim);;
         start_index = end_index + 1,
                     end_index = view.find_first_of(Delim, start_index)) {
      *(iter++) = view.substr(start_index, end_index - start_index);

      if (end_index == std::string_view::npos) {
        break;
      }
    }

    return result;
  }();
};

template 
inline constexpr auto&& split_of_v = split_of::value;

template 
struct split {
  static constexpr std::string_view view{S};
  static constexpr auto value = [] {
    constexpr auto group_count = [] {
      std::size_t count{};
      std::size_t index{};

      while ((index = view.find(Delim, index)) != std::string_view::npos) {
        count++;
        index += Delim.size();
      }

      return count + 1;
    }();
    std::array result{};

    auto iter = result.begin();

    for (std::size_t start_index = 0, end_index = view.find(Delim);;
         start_index = end_index + Delim.size(),
                     end_index = view.find(Delim, start_index)) {
      *(iter++) = view.substr(start_index, end_index - start_index);

      if (end_index == std::string_view::npos) {
        break;
      }
    }

    return result;
  }();
};

template 
inline constexpr auto&& split_v = split::value;

template 
struct remove_char {
  static constexpr auto value = [] {
    struct removal_metadata {
      decltype(S) result;
      std::size_t actual_size;
    };

    constexpr auto metadata = [] {
      auto result = S;
      auto removal_end = std::remove(result.begin(), result.end(), C);

      return removal_metadata{
          .result{std::move(result)},
          .actual_size{static_cast(removal_end - result.begin())}};
    }();

    meta_string result;

    std::copy(metadata.result.begin(),
              metadata.result.begin() + metadata.actual_size, result.begin());

    return result;
  }();
};

template 
inline constexpr auto&& remove_char_v = remove_char::value;

template 
struct remove {
  static constexpr auto groups = split_v;
  static constexpr auto value = [] {
    return [](std::index_sequence) {
      return meta_string{std::span{
          groups[Is].data(), groups[Is].size()}...};
    }
    (std::make_index_sequence{});
  }();
};

template 
inline constexpr auto&& remove_v = remove::value;
#endif
}  // namespace refvalue

refvalue::meta_string 的主要作用如下:

1. 编译时字符串操作: refvalue::meta_string 允许在编译时对字符串进行操作,如连接、截取、比较等。这样可以在编译时进行字符串相关的计算和处理,而不需要在运行时进行。

2. 代码生成: refvalue::meta_string 可以用于生成代码,特别是与字符串相关的代码。通过在编译时生成字符串字面量,可以将字符串作为代码的一部分,并在代码生成过程中进行操作和处理。

3. 元数据处理: refvalue::meta_string 可以用于处理字符串类型的元数据,如类名、函数名等。通过在编译时生成字符串字面量,可以在元编程中使用这些字符串作为类型和标识符的名称。

meta_string 的设计旨在在编译期间进行字符串操作,以提高性能。由于操作发生在编译期,因此可以避免运行时的字符串操作开销,从而提高程序的执行效率。 举例来说,通过使用 meta_string ,可以在编译期间拼接字符串、提取子串、移除字符等操作,而无需在运行时进行这些操作。这使得代码更加灵活和高效。

总之, refvalue::meta_string 是一个元编程工具,用于在编译时生成字符串字面量,并进行字符串相关的操作和处理。它可以用于生成代码、处理元数据,并在编译时进行字符串计算和操作,提供了一种在编译时进行字符串处理的能力。

需要注意的是,该代码中的一些功能可能需要C++20或更高版本的支持,例如 std::span 和 std::same_as 等特性。因此,在使用这些特性时,需要确保编译器支持它们。

meta_string使用举例

#include 
#include "meta_string.hpp"

int main() {
    using namespace refvalue;

    constexpr auto str = meta_string("hello world");

    static_assert(str.size() == 6, "字符串长度不正确");

    std::cout << "字符串: " << str << std::endl;
    std::cout << "字符串长度: " << str.size() << std::endl;

    constexpr auto subStr = str.substr<3, 2>();

    std::cout << "子串: " << subStr << std::endl;
    std::cout << "子串长度: " << subStr.size() << std::endl;

    constexpr auto concatStr = str + meta_string(" 欢迎!");

    std::cout << "拼接后的字符串: " << concatStr << std::endl;

    constexpr auto removedStr = remove_v;

    std::cout << "移除后的字符串: " << removedStr << std::endl;

    return 0;
}

在以上示例中,创建了一个 meta_string 对象 str 并初始化, 然后我们展示了一些对 meta_string 的操作。使用 std::cout 打印原始字符串及其长度,使用 substr 函数提取了一个子串,指定了起始位置和长度,并打印了子串及其长度。 我们使用 + 运算符将 str 与另一个 meta_string 拼接起来,并将结果存储在 concatStr 中。 最后使用 remove_v 函数从 concatStr 中移除了子串"迎",并将结果存储在 removedStr 中。constexpr 关键字用于确保这些操作在编译时进行求值。  

meta_string 允许在编译时进行字符串操作,而不是在运行时。这意味着字符串处理的结果在程序执行之前就已经确定,可以提高程序的性能和效率。 由于字符串处理发生在编译时,而不是运行时,因此可以避免在程序执行期间进行字符串操作的开销,这可以减少运行时的计算和内存消耗。这也是easylog中使用meta_string 的原因,像一些日志格式固定的前缀字符如日期,代码行号之类的场景,用它能够显著提高性能。

type traits机制

type traits这个概念,直译就是类型萃取。根据名字也能猜到是用于获取类型的,在c++ 11之前,stl就已经用到了相关技术了,比如迭代器使用相关的类型获取,《STL 源码剖析》有详细介绍,有兴趣的可以去看看。c++ 11更是引入了一个专门的头文件用来做type traits的相关事情。

简单理解,C++中的type traits是一种编译时类型信息查询和操作的机制。它们允许我们在编译时获取有关类型的属性和特征,并根据这些特征进行编程。 Type traits可以帮助我们判断一个类型是否具有某些特性,例如是否是指针、引用、数组等,是否是const或volatile限定符,或者是否可以进行拷贝构造或移动构造等操作。通过这些traits,我们可以在编译时根据类型的特征进行不同的处理和逻辑分支。

简单来说,type traits就是一种在编译时查询和操作类型属性的工具,它们可以帮助我们根据类型的特征进行编程,提高代码的可靠性和灵活性。

template 
struct function_traits {
  using parameters_type = void;
  using return_type = Return;
};

在以上给定的代码片段中,定义了一个 function_traits 模板的特化,用于没有参数且带有noexcept指定符的函数指针。当模板参数与 Return (*)() noexcept 模式匹配时,将触发此特化。 在特化中,定义了两个类型别名: parameters_typereturn_type 。在这种情况下, parameters_type 被设置为 void ,表示函数没有参数,而 return_type 被设置为模板参数 Return ,表示函数的返回类型。 这段代码在您想要提取或操作函数类型的信息时很有用,例如提取返回类型或检查函数是否具有特定的签名。像 function_traits 这样的类型特性可以在需要编译时类型信息的模板元编程或通用编程场景中使用。 

举个简单的使用示例加深理解:

#include 
#include 

// 类型特性,用于检查一个类型是否为指针
template 
struct is_pointer {
    static constexpr bool value = false;
};

template 
struct is_pointer {
    static constexpr bool value = true;
};

// 使用类型特性
int main() {
    std::cout << std::boolalpha;
    std::cout << "int* 是指针吗?" << is_pointer::value << std::endl;
    std::cout << "float* 是指针吗?" << is_pointer::value << std::endl;
    std::cout << "double 是指针吗?" << is_pointer::value << std::endl;

    return 0;
}

在这个示例中,定义了一个名为 is_pointer 的类型特性,用于检查给定的类型是否为指针。这个特性被定义为一个结构体模板,包含一个静态成员变量 value ,默认情况下设置为 false 。然后为 is_pointer 提供了一个偏特化,其中 T* 表示指针类型。在这个偏特化中,将 value 设置为 true 。 在 main 函数中使用 is_pointer 类型特性来检查不同类型是否为指针。使用 std::cout 打印结果。对于 int*float* ,输出将为 true ,因为它们是指针类型;对于 double ,输出将为 false ,因为它不是指针类型。 这个例子展示了如何使用类型特性在编译期进行类型检查,并在编译期提供有关类型的信息。 

std::string_view

easylog中大量使用了std::string_view。

std::string_view是C++17中引入的一个轻量级字符串视图类。

优点

1. 零开销抽象:std::string_view不拥有任何数据,它只是对现有字符串数据的一个视图。这意味着它可以非常高效地构造和传递,没有数据复制的开销。

2. 与std::string互操作:std::string_view可以方便地从std::string构造,反之亦然。这使其非常适合作为函数参数,可以接受std::string或std::string_view。

3. 安全性:与C风格字符串相比,std::string_view是类型安全的,可以避免潜在的缓冲区溢出错误。

4. 灵活性:std::string_view可以指向任何连续的字符序列,不仅限于std::string。这使其非常通用和灵活。

适用场景

1. 当需要一个字符串抽象,但不需要拥有底层字符串数据时。例如,在函数参数中,您可能只需要读取字符串数据,而不需要拥有或修改它。

2. 当需要一个字符串视图,可以指向不同的字符串类型(如std::string、char*、const char*等)时。 3. 当您需要高效地构造和传递字符串数据时,以避免不必要的复制开销。

4. 当需要一个类型安全的字符串抽象时,可以替代C风格字符串。

5. 当需要一个可以与std::string互操作的字符串视图时,以支持接受std::string或std::string_view的函数。

总之,std::string_view是一个轻量级的字符串抽象,可以高效地操作字符串数据,并提供类型安全性和与std::string的互操作性。它非常适合作为函数参数,或在需要字符串视图而不必拥有底层数据时使用。

doctest单元测试框架

easylog中使用了doctest单元测试框架。doctest 是 C++ 的一个单元测试框架,与 Google Test(gtest) 相比,其主要优点:

1. 简单易用:doctest 的 API 非常简单,只需要几个宏定义和注释就可以写出测试用例。相比之下,gtest 的 API 稍微复杂一些,需要更多的函数和类来定义测试。

2. 无需链接库:doctest 是一个头文件库,不需要链接任何库文件。gtest 则需要链接 libgtest.a 和 libgtest_main.a 两个库文件。

3. 支持子测试:doctest 支持将测试用例组织成层级的子测试,这使得测试更加结构化和可读。gtest 目前不支持子测试。

4. 支持多种断言:doctest 提供了多种断言宏用于验证测试条件,包括相等性断言、真值断言、异常断言等。gtest 也提供了多种断言,但稍微少一些。

5. 支持测试用例的标签和忽略:doctest 可以给测试用例添加标签,然后选择只运行带有某个标签的测试用例。它也支持忽略指定的测试用例。gtest 不支持这两个特性。

6. 支持多种测试报告:doctest 可以生成多种格式的测试报告,包括 XML、JSON、JUnit 等。gtest 只支持 Google Test 格式的报告。

7. 支持随机测试用例顺序:doctest 可以随机改变测试用例的执行顺序,这在检测依赖测试用例顺序的 bug 时很有用。gtest 不支持随机测试顺序。

总的来说,doctest 是一个简单、易用和功能更加强大的 C++ 单元测试框架,相比 gtest 具有更多的特性和优点,简单易用,适合用于一些 C++ 项目的单元测试。

easylog使用

easylog使用较简单,可以直接参考示例。

  std::string filename = "easylog.txt";
  std::filesystem::remove(filename);
  easylog::init_log(Severity::DEBUG, filename, true, 5000, 1, true);

  ELOG_INFO << 42 << " " << 4.5 << 'a' << Severity::DEBUG;

  ELOGV(INFO, "test");
  ELOGV(INFO, "it is a long string test %d %s", 2, "ok");

  int len = 42;
  ELOGV(INFO, "rpc header data_len: %d, buf sz: %lu", len, 20);

  ELOG(INFO) << "test log";
  easylog::flush();
  ELOG_INFO << "hello "
            << "easylog";

  ELOGI << "same";

  ELOG_DEBUG << "debug log";
  ELOGD << "debug log";

c++17支持

注,原始的仓库,默认是不支持c++17,最低要求是c++20。但是可以修改几处代码,以支持c++17。

type_traits.h中启用以下定义。

namespace std {
template 
struct remove_cvref {
  typedef std::remove_cv_t> type;
};
template 
using remove_cvref_t = typename remove_cvref::type;
}  // namespace std

meta-string.h中去除 span相关的头文件和定义。因为是c++20新增的特性。

其他资源

高效C++无锁(lock free)队列moodycamel::ConcurrentQueue_草上爬的博客-CSDN博客

https://github.com/jk-jeon/dragonbox

mirrors / cameron314 / concurrentqueue · GitCode

mirrors / alibaba / yalantinglibs · GitCode

https://github.com/KjellKod/g3log

GitHub - purecpp-org/easylog: a c++20 log lib

【日志工具】g3log_1_简介_泡泡吐泡泡啊的博客-CSDN博客

https://github.com/purecpp-org/easylog/tree/da2ed3a8e74b29a73faa67896dac02e1b7584551

C++的那些事——std::string_view和std::span - 知乎

萃取(traits)编程技术的介绍和应用_traits编程_Hello,C++!的博客-CSDN博客

C++11 类型支持之type traits_type_traits_wxj1992的博客-CSDN博客

你可能感兴趣的:(C++,c++,开发语言)