fmt库如何编译期识别出任意类型

一、问题

fmt库最为神奇的地方是好像在编译时记录出了所有参数的类型,并将这类型一直保留到运行时使用。其实“编译时记录”所有参数类型并不神奇,这一点在C++11中的模板参数包(template parameter pack)的支持下比较容易实现,更为方便的是直接使用std提供的tuple功能,可以方便的在编译时记录下所有的参数类型和参数值。但这里的问题在于tuple使用的时候,如果要取某个元素,需要提供编译时常量,这一点对于通用的功能库来说是不能接受的。一个直观的基类所有参数的函数可以这样实现:
(gdb) p tup
$1 = std::tuple containing = {[1] = 0, [2] = 0.10000000000000001, [3] = 0x4008d3 "hello"}
(gdb) bt
#0 recordvar<int, double, char const*> (fmt=0x4008d9 "") at tuple.record.cpp:6
#1 0x00000000004005e6 in main (argc=1, argv=0x7fffffffe508) at tuple.record.cpp:12
(gdb) p tup
$2 = std::tuple containing = {[1] = 0, [2] = 0.10000000000000001, [3] = 0x4008d3 "hello"}
(gdb) shell cat -n tuple.record.cpp
1 #include <tuple>
2 template<typename... types>
3 void recordvar(const char *fmt, types... args)
4 {
5 std::tuple<types...> tup(args...);
6 int x = 1;
7
8 }
9
10 int main(int argc, const char *argv[])
11 {
12 recordvar("", 0, 0.1, "hello");
13 }
(gdb)
虽然记录了所有的类型和参数,但是这个内容是狗咬刺猬——无从下口,接下来就没办法使用了。

二、这个问题的核心难点

直观的理解,为了实现对于任意类型(包括自定义类型)的格式化,需要记录该类的类型,然后通过模板来格式化。由于格式化的时候通常可以通过"{1}"来访问指定位置的格式化参数,所以这些参数应该以数组的形式放在一起。进一步推导,如果所有的参数放在一个数组中,那么它们就需要具有相同的类型。这就又涉及到一个无法完成的任务,因为它们的类型并不相同,不可能抽象出一个适合所有类型的数组类型。注意,到这时候,这个库需要解决的核心痛点出现了:就是如何提炼出编译时统一的类型,这个类型能够将所有格式化参数(包括数值和类型)都放在其中。

三、fmt解决该问题的思路

如果想把所有的类型放在一起,对于任意自定义类型,它们能够统一的方式就是void*,所以将所有用户自定义类型的参数值作为指针记录起来,这样可以轻松的在编译时知道某个格式化参数在内存中的位置,这一点也非常简单,难点在于如何解析这个参数的类型,或者说,它的类型怎么确定呢(在保证保存所有形参的数组具有相同形式的前提下)?
其实fmt的解决思路同样是基于void*指针,只是这个时候把void*作为了函数指针来使用。
tsecer@harry: cat -n fmt.gen.fun.cpp
1 #include <stdio.h>
2 template<typename T>
3 void convfun(T *pt)
4 {
5 }
6
7 template<>
8 void convfun(int *parg)
9 {
10 printf("tsecer %d\n", *parg);
11 }
12
13 template<>
14 void convfun(float *pf)
15 {
16 printf("tsecer %f\n", *pf);
17 }
18
19 struct A
20 {
21 int x;
22 };
23
24 template<>
25 void convfun(A *pa)
26 {
27 printf("tsecer %d\n", pa->x);
28 }
29
30 template <typename T>
31 void generalfn(void *parg)
32 {
33 convfun<T>((T*)parg);
34 }
35
36 template<typename T>
37 struct fmtvalue
38 {
39 fmtvalue(void* a): parg(a), pfn(generalfn<T>){}
40 void *parg;
41 using formatfn = void(void *);
42 formatfn *pfn;
43 };
44
45 int main(int argc, const char *argv[])
46 {
47 int i = 11;
48 float f = 3.14;
49 A a = {22};
50 fmtvalue<int> fi(&i) ;
51 fmtvalue<float> ff(&f);
52 fmtvalue<A> fa(&a);
53 using ut = fmtvalue<int>;
54 fmtvalue<int> *arr[3] = { (ut*)&fi, (ut*)&ff, (ut*)&fa};
55 for (int i = 0; i < sizeof(arr)/sizeof(arr[0]); i++)
56 {
57 arr[i]->pfn(arr[i]->parg);
58 }
59 return 0;
60
61 }
tsecer@harry: g++ -g -std=c++11 fmt.gen.fun.cpp
tsecer@harry: ./a.out
tsecer 11
tsecer 3.140000
tsecer 22
tsecer@harry:
注意在上面的代码中,已经可以通过运行时变量i来获得任意类型的格式化输出了。这里tricky的地方在于,作为数组的类型为fmtvalue<int>,
54 fmtvalue<int> *arr[3] = { (ut*)&fi, (ut*)&ff, (ut*)&fa};
这种类型中有两个void指针,一个是参数的内存地址,另一个是格式化函数的地址,这个格式化函数使用的参数是void类型,但是它初始化的时候使用了模板,从而在编译时迫使编译器为这个类型生成了特定类型的对应格式化函数。也就是说,fmt库并没有尝试在一个类型中保存自定义参数的类型,而只是记录了格式化(模板)函数的指针位置,在需要格式化的时候直接调用对应类型的格式化函数即可,这个也是每个自定义类型格式化时需要提供(特例化)的模板函数。

四、自定义类型需要实现的接口

fmt-master\include\fmt\core.h
// A formatting argument value.
template <typename Context> class value {
public:
using char_type = typename Context::char_type;
……
private:
// Formats an argument of a custom type, such as a user-defined class.
template <typename T, typename Formatter>
static void format_custom_arg(const void* arg,
typename Context::parse_context_type& parse_ctx,
Context& ctx) {
Formatter f;
parse_ctx.advance_to(f.parse(parse_ctx));
ctx.advance_to(f.format(*static_cast<const T*>(arg), ctx));
}
};
也就是需要特例化parse和format两个函数,其中parse是解析格式化中的字符串(为了支持自定义类型的自定义格式),而format则是生成格式化之后的字符串。
fmt-master\test\core-test.cc
FMT_BEGIN_NAMESPACE
template <> struct formatter<custom_type> {
auto parse(format_parse_context& ctx) const -> decltype(ctx.begin()) {
return ctx.begin();
}

template <typename FormatContext>
auto format(const custom_type& p, FormatContext& ctx) -> decltype(ctx.out()) {
return format_to(ctx.out(), "cust={}", p.i);
}
};
FMT_END_NAMESPACE

五、以名字访问的参数

比较有意思的是fmt还支持一种不太常用的以名字访问参数的方法,网上的文档比较少,大致的访问方法为
tsecer@harry: cat -n arg-id.cpp
1 #include "../../include/fmt/format.h"
2
3 int main(int argc, const char *argv[])
4 {
5 fmt::print("{1} {x}", fmt::arg("x", "world\n"), "hello");
6 return 0;
7 }
tsecer@harry: g++ -std=c++11 -I ../../include arg-id.cpp -L ../.. -lfmt
tsecer@harry: ./a.out
hello world
tsecer@harry:

posted on 2020-11-26 19:11  tsecer  阅读(446)  评论(0编辑  收藏  举报

导航