前文

日志库spdlog(一) 源码面前,了无秘密 spdlog · Code Analysis 正如前文所说,本文将聚焦于spdlog在并发方面的工作,同时提点一下值得一提的技术。

stopwatch

大家都应该经历过要为某一段代码计时,通常来说我们大概率会写出以下代码:

 1auto start = std::chrono::steady_clock::now();
 2
 3// ===== 要计时的代码 =====
 4
 5auto end = std::chrono::steady_clock::now();
 6
 7auto duration =
 8    std::chrono::duration_cast<std::chrono::microseconds>(
 9        end - start
10    );
11
12std::cout << "耗时: " << duration.count() << " us\n";

但如果有多段要计时的代码,就不得不重复写这些东西,spdlog里就有个很有意思的工具类:stopwatch

 1class stopwatch {
 2    using clock = std::chrono::steady_clock;
 3    std::chrono::time_point<clock> start_tp_;
 4
 5public:
 6    stopwatch()
 7        : start_tp_{clock::now()} {}
 8
 9    std::chrono::duration<double> elapsed() const {
10        return std::chrono::duration<double>(clock::now() - start_tp_);
11    }
12
13    std::chrono::milliseconds elapsed_ms() const {
14        return std::chrono::duration_cast<std::chrono::milliseconds>(clock::now() - start_tp_);
15    }
16
17    void reset() { start_tp_ = clock::now(); }
18};

只需要创建一个类,然后通过reset就可以重复使用,实在是太过精妙。

spdlog里大量使用了宏,因为这个库要足够通用,在不同的平台、针对不同的C++版本都要进行适配,这里有几个有意思的用法:

  1. 兼容不同版本的fmt 众所周知,fmt库在C++20被吸收到了标准库里,所以针对不同的C++版本,可以使用来指定使用不同的库:
1#ifdef SPDLOG_USE_STD_FORMAT
2    std
3#else
4    fmt
5#endif

另一个则是语法的适配,比如make_unique在C++14才被引入:

 1#if __cplusplus >= 201402L  // C++14 and beyond
 2using std::enable_if_t;
 3using std::make_unique;
 4#else
 5template <bool B, class T = void>
 6using enable_if_t = typename std::enable_if<B, T>::type;
 7
 8template <typename T, typename... Args>
 9std::unique_ptr<T> make_unique(Args &&...args) {
10    static_assert(!std::is_array<T>::value, "arrays not supported");
11    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
12}
13#endif

这里甚至自己捏了一个。

  1. 批量控制宏的开关 比如现在我想使用库的方式链接spdlog,那显然有些操作就不能再用了,比如inline,此时就必须依赖宏在中间隔离一层:
 1#ifdef SPDLOG_COMPILED_LIB
 2#undef SPDLOG_HEADER_ONLY
 3#if defined(SPDLOG_SHARED_LIB)
 4#if defined(_WIN32)
 5#ifdef spdlog_EXPORTS
 6#define SPDLOG_API __declspec(dllexport)
 7#else  // !spdlog_EXPORTS
 8#define SPDLOG_API __declspec(dllimport)
 9#endif
10#else  // !defined(_WIN32)
11#define SPDLOG_API __attribute__((visibility("default")))
12#endif
13#else  // !defined(SPDLOG_SHARED_LIB)
14#define SPDLOG_API
15#endif
16#define SPDLOG_INLINE
17#else  // !defined(SPDLOG_COMPILED_LIB)
18#define SPDLOG_API
19#define SPDLOG_HEADER_ONLY
20#define SPDLOG_INLINE inline
21#endif  // #ifdef SPDLOG_COMPILED_LIB

兼容模板

对于单线程的情况,如果实在不需要锁,那此时甚至没必要传入一个有效的锁给sink,否则会有性能损耗。 sink说到底只是需要一个可以lock、unlock的东西(实际上是lock_guard需要),那么可以提供给他,并不会有什么性能损耗:

1struct null_mutex {
2    void lock() const {}
3    void unlock() const {}
4};

这里可以顺带提一嘴lock_guard,其实无非就是把lock写在构造,把unlock放在析构:

 1template <class _Mutex>
 2class _LIBCPP_SCOPED_LOCKABLE lock_guard {
 3public:
 4  typedef _Mutex mutex_type;
 5
 6private:
 7  mutex_type& __m_;
 8
 9public:
10  [[__nodiscard__]] _LIBCPP_HIDE_FROM_ABI explicit lock_guard(mutex_type& __m) _LIBCPP_ACQUIRE_CAPABILITY(__m)
11      : __m_(__m) {
12    __m_.lock();
13  }
14
15  [[__nodiscard__]] _LIBCPP_HIDE_FROM_ABI lock_guard(mutex_type& __m, adopt_lock_t) _LIBCPP_REQUIRES_CAPABILITY(__m)
16      : __m_(__m) {}
17  _LIBCPP_RELEASE_CAPABILITY _LIBCPP_HIDE_FROM_ABI ~lock_guard() { __m_.unlock(); }
18
19  lock_guard(lock_guard const&)            = delete;
20  lock_guard& operator=(lock_guard const&) = delete;
21};

省略看不懂的东西之后,我想不难理解其中含义,毕竟标准库大部分还是符合C++语法的,只是少部分用了魔法操作。

enable_shared_from_this

常规而言,无法在类内创建一个自身的shared_ptr,因为不知道外面的调用者是否是通过shared_ptr调用,如果随意创建,则会导致double free。 好在标准库提供了enable_shared_from_this这样一个工具(CRTP的经典应用),只要继承该类模板,就可以安全的在类内创建shared_ptr。 spdlog当然也有使用,这里就不再重复贴了。

不难想象,这里的实现是任何通过make_shared创建的对象都会检查是否有继承enable_shared_from_this。 如果继承了,那显然就要设置一些额外的数据到enable_shared_from_this那一部分,比如,一个weak_ptr。

 1template <class _Tp>
 2class enable_shared_from_this {
 3  mutable weak_ptr<_Tp> __weak_this_;
 4
 5protected:
 6  _LIBCPP_HIDE_FROM_ABI _LIBCPP_CONSTEXPR enable_shared_from_this() _NOEXCEPT {}
 7  _LIBCPP_HIDE_FROM_ABI enable_shared_from_this(enable_shared_from_this const&) _NOEXCEPT {}
 8  _LIBCPP_HIDE_FROM_ABI enable_shared_from_this& operator=(enable_shared_from_this const&) _NOEXCEPT { return *this; }
 9  _LIBCPP_HIDE_FROM_ABI ~enable_shared_from_this() {}
10
11public:
12  _LIBCPP_HIDE_FROM_ABI shared_ptr<_Tp> shared_from_this() { return shared_ptr<_Tp>(__weak_this_); }
13  _LIBCPP_HIDE_FROM_ABI shared_ptr<_Tp const> shared_from_this() const { return shared_ptr<const _Tp>(__weak_this_); }
14
15#if _LIBCPP_STD_VER >= 17
16  _LIBCPP_HIDE_FROM_ABI weak_ptr<_Tp> weak_from_this() _NOEXCEPT { return __weak_this_; }
17
18  _LIBCPP_HIDE_FROM_ABI weak_ptr<const _Tp> weak_from_this() const _NOEXCEPT { return __weak_this_; }
19#endif // _LIBCPP_STD_VER >= 17
20
21  template <class _Up>
22  friend class shared_ptr;
23};

也没有什么特别看不懂的东西,只有基础的C++语法,当然CRTP这个东西本身就比较tricky。

多态clone惯用法

如果有一个基类衍生出巨多派生类,并且你要存这些派生类的实例,此时你大概会想要以指针/引用的方式存储其基类:

1std::unique_ptr<formatter> formatter_;

此时如果需要复制呢?显然一方面没办法通过基类来复制,这样只会复制基类的部分,也没办法获取真实类型。 所以就有了clone惯用法,比如,基类定义clone方法,再由子类重写该方法,便可以依赖多态的特性,比较舒适地复制:

 1// formatter.h
 2// 纯接口类
 3class formatter {
 4public:
 5    virtual ~formatter() = default;
 6    // 后续都走这个来做format,将msg内容格式化到dest中
 7    virtual void format(const details::log_msg &msg, memory_buf_t &dest) = 0;
 8    virtual std::unique_ptr<formatter> clone() const = 0;
 9};
10
11// pattern_fromatter-inl.h
12std::unique_ptr<formatter> pattern_formatter::clone() const {
13    custom_flags cloned_custom_formatters;
14    for (auto &it : custom_handlers_) {
15        cloned_custom_formatters[it.first] = it.second->clone();
16    }
17    auto cloned = details::make_unique<pattern_formatter>(pattern_, pattern_time_type_, eol_,
18                                                          std::move(cloned_custom_formatters));
19    cloned->need_localtime(need_localtime_);
20#if defined(__GNUC__) && __GNUC__ < 5
21    return std::move(cloned);
22#else
23    return cloned;
24#endif
25}

SFINAE

在log的转发处,有这么一个函数:

 1// 非格式化的二级转发入口,非string类型
 2// T cannot be statically converted to format string (including string_view/wstring_view)
 3template <class T,
 4            typename std::enable_if<!is_convertible_to_any_format_string<const T &>::value,
 5                                    int>::type = 0>
 6void log(source_loc loc, level::level_enum lvl, const T &msg) {
 7    // 不能转成字符串的,非格式化的转发入口
 8    // 等价于给普通类型自动套上一个{}
 9    // 二级转发入口
10    log(loc, lvl, "{}", msg);
11}

这里有一段东西初看十分迷惑:

1typename std::enable_if<!is_convertible_to_any_format_string<const T &>::value, int>::type = 0

其实这里用到了一个模板技巧:Substitution Failure Is Not An Error,说人话就是模版匹配失败并不报错,而是将其淘汰,即不满足要求,就不要选。 而enable_if则是std提供的工具,前面一个非类型参数为true时,可以拿到type,类型为第二个类型参数。 这个功能常被用于匹配类型,比如现在我的模板函数需要一个是整数的类型,那我可以这么写:

1template<class T, std::enable_if_t<is_intergral_v<T>, int> = 0>
2func(T arg1) {
3    // ...
4}

可以简单理解下,这里第二个参数=0是为了给一个默认值,别人调用func就可以只塞第一个参数,由于第二个非类型参数没用到,所以名称都可以省略。 enable_if、is_intergral的实现有兴趣也可以看看,简单的模版偏特化。 而enable_if_t、is_intergral_v则是简单的语法糖,避免像上面一样写::type::value 假设一个非整数类型调用了该函数,那么enable_if_t什么都返回不了,参数没有类型,故此匹配失败。

而后的is_convertible_to_any_format_string也是一个比较魔法的操作:

 1template <class T>
 2struct is_convertible_to_any_format_string
 3    : std::integral_constant<bool,
 4                             is_convertible_to_basic_format_string<T, char>::value ||
 5                                 is_convertible_to_basic_format_string<T, wchar_t>::value> {};
 6
 7template <class T, class Char = char>
 8struct is_convertible_to_basic_format_string
 9    : std::integral_constant<bool,
10                             std::is_convertible<T, fmt::basic_string_view<Char>>::value ||
11                                 std::is_same<remove_cvref_t<T>, fmt_runtime_string<Char>>::value> {
12};

这里就愈发晦涩了,不过也可以简单理解。 我们要判断一个类型参数,尽可能满足value、type那一套,此时又不想写一大串偏特化,就可以继承integral_constant,其内部定义了value,值为后者,定义了type,类型为T(其实还有个value_type,但这里没用到,不提也罢)。 例如,传入一个char:

  1. 显然std::is_convertible<T, fmt::basic_string_view<Char>>::value为true
  2. 那么is_convertible_to_basic_format_string<T, char>::value为true
  3. is_convertible_to_any_format_string<const T &>::value为true
  4. std::enable_if<!is_convertible_to_any_format_string<const T &>::value, int>::type就不存在了,类型参数不能不存在,所以匹配失败

其实这一坨就是对类型参数的约束,某个类型满足要求可以,不满足要求就绝对不行。 C++20对模板匹配语法也有增强,提出了一个concept的东西,就是可以便捷声明类型需要满足的约束条件。

总结

该库大部分的代码都是在做各种兼容,兼容平台、兼容C++版本、兼容引入方式等等,其工程实践十分值得一学。 这三篇分析未必足够完备,但对spdlog库的分析至此告一段落。 我相信哪怕不看这些文章,对着源码耐心看看,这个库也并不难理解,没有太多神秘之处。 在现在的条件下,我想实在没有任何阻力阻止你去学习一份优秀的源码。 以前还可以说这里我实在看不懂,被卡住了,但现在哪怕免费的AI也能给你答疑解惑。 笔者也是边看边学,读到不懂的地方就问问AI。

身处这种变革的时代,既幸运又不幸,也不知道还能写几年代码😭。 下课!