前文
日志库spdlog(一) 源码面前,了无秘密 正如前文所说,本文将聚焦于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++版本都要进行适配,这里有几个有意思的用法:
- 兼容不同版本的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
这里甚至自己捏了一个。
- 批量控制宏的开关 比如现在我想使用库的方式链接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:
- 显然
std::is_convertible<T, fmt::basic_string_view<Char>>::value为true - 那么
is_convertible_to_basic_format_string<T, char>::value为true - 有
is_convertible_to_any_format_string<const T &>::value为true std::enable_if<!is_convertible_to_any_format_string<const T &>::value, int>::type就不存在了,类型参数不能不存在,所以匹配失败
其实这一坨就是对类型参数的约束,某个类型满足要求可以,不满足要求就绝对不行。 C++20对模板匹配语法也有增强,提出了一个concept的东西,就是可以便捷声明类型需要满足的约束条件。
总结
该库大部分的代码都是在做各种兼容,兼容平台、兼容C++版本、兼容引入方式等等,其工程实践十分值得一学。 这三篇分析未必足够完备,但对spdlog库的分析至此告一段落。 我相信哪怕不看这些文章,对着源码耐心看看,这个库也并不难理解,没有太多神秘之处。 在现在的条件下,我想实在没有任何阻力阻止你去学习一份优秀的源码。 以前还可以说这里我实在看不懂,被卡住了,但现在哪怕免费的AI也能给你答疑解惑。 笔者也是边看边学,读到不懂的地方就问问AI。
身处这种变革的时代,既幸运又不幸,也不知道还能写几年代码😭。 下课!