|
5. Learn TMP in use (Part II)
5.1 Example 4: SFINAE
本节展示两个基于 SFINAE 的例子,enable_if 和 void_t。
5.1.1 enable_if
假设我们想要这样的函数模板重载,对于整型的参数,匹配第一个模板,这个模板实现一些针对整型参数的逻辑;
对于浮点数型的参数,匹配第二个模板,这个模板实现针对浮点型参数的逻辑。
于是我们定义了下面两个函数模板重载:
- // The first one is for integral type such as int, char, long...
- template <typename INT> void foo(INT) {}
- // The second one is for floating point types such as float, double...
- template <typename FLT> void foo(FLT) {} // Error: redefinition!
复制代码
但是这时我们发现,编译器报告了一个重定义错误,上面两个模板实际上是一模一样的,因为它们的签名完全相同。
我们无法写一个针对参数类型的函数模板重载。唯一的办法是对每一个类型,int, char, long, float, double... 都写一个普通函数重载:
- void foo(int) {}
- void foo(char) {}
- void foo(float) {}
- // ...
复制代码
但是这样我们要定义十几个相似的函数,完全失去了泛型编程的支持。这时,我们就需要 enable_if 出马了:
- template <bool, typename T = void>
- struct enable_if : type_identity<T> {};
- template <typename T>
- struct enable_if<false, T> {};
- template <typename T> enable_if_t<is_integral_v<T>> foo(T) {} // #1
- template <typename T> enable_if_t<is_floating_point_v<T>> foo(T) {} // #2
- foo(1); // match #1
- foo(1.0f); // match #2
复制代码
我们先来看 enable_if 是怎么定义的。
首先它是一个类模板,主模板有两个形参,第一个形参接受一个 bool 值,第二个形参接受一个类型 T 且有一个默认值 void,主模板就返回 T 本身。
另外有一个偏特化,偏特化接受一个类型形参 T,它匹配 bool 值为 false 的情况,但注意,它内部没有定义 “type”!
这就造成一种效果,当我们 “调用” enable_if<bool, T>::type 时,如果 bool 值为 true,那么 enable_if 就返回 T;
而如果 bool 值为 false,enable_if 返回不了任何东西!
“type” 不存在,对它的调用构成了一个非良构的(ill-formed)表达式,只要这个表达式位于立即上下文(Immediate Context)中,那么就会触发 SFINAE 机制!
所以,对于 foo(1),在函数模板 #2 中,T 被推导为 int,is_floating_point_v<T> 为 false,
所以 enable_if_t<is_floating_point_v<T>> 产生了非良构的实例化表达式。
我们看下这个表达式位于哪里,它位于函数模板 foo 的返回值处,这里是一个立即上下文,所以这是一个替换失败(Substitution Failure),
函数模板 #2 从重载集中剔除,避免了重定义错误。
foo(1.0f) 也是同理。
所以,通过 enable_if(更准确地说是通过 SFINAE),我们拥有了基于逻辑来控制函数重载集的能力,
而原本的我们仅仅只能基于 C++ 语法规则来控制重载,这就是 TMP 的威力。
我们来看第二个例子,假设我们想通过类模板的形参来控制类模板成员函数的重载:
- template <typename T> struct S {
- template <typename U> static enable_if_t< is_same_v<T, int>> foo(U) {} // #1
- template <typename U> static enable_if_t<!is_same_v<T, int>> foo(U) {} // #2
- };
- S<int>::foo(1);
复制代码
在这里,foo 是 S 的静态成员函数模板,我们希望,当 S 的实参是 int 时,匹配第一个 foo;
当 S 的实参不是 int 时,匹配第二个 foo。也就是说,我们希望 S<int>::foo(1) 调用的是 #1。
但是,我们去编译这段代码却发现编译器报告了错误:
- metaprogramming/immediate_context.cpp:44:40:
- error: no type named 'type' in 'enable_if<false>';
- 'enable_if' cannot be used to disable this declaration
- using enable_if_t = typename enable_if<B, T>::type;
- ^
- metaprogramming/immediate_context.cpp:52:10:
- note: in instantiation of template type alias 'enable_if_t' requested here
- static enable_if_t<!is_same_v<int, T>> foo(U) {}
- ^
- metaprogramming/immediate_context.cpp:55:14:
- note: in instantiation of template class 'S<int>' requested here
- int main() { S<int>::foo(1.0f); }
复制代码
我们分析下错误信息,编译器说在实例化 S<int> 的时候,enable_if<false> 里面没有名叫 “type” 的类型。
这不是我们预期的替换失败吗?怎么没有 SFINAE,而是报告了一个错误?
历史经验告诉我们,当编译器和我们的预期不一致时,一般都是我们错了。
那么是哪里搞错了呢?仔细分析我们发现,原来是因为这个 enable_if 不在类模板 S 的立即上下文里!
奇怪,enable_if 不是在 foo 的返回值里的吗?在上个例子里返回值不是立即上下文吗?
注意,foo 的返回值属于立即上下文,是对 foo 来说的。
也就是说,在实例化成员函数模板 foo 的时候,foo 的返回值区域位于立即上下文中。
而我们现在正在实例化 S,只有在 S 的自己的立即上下文内才能使用 SFINAE。
所以我们就没办法通过类模板的实参控制成员函数重载了吗?
不是的,有一个巧妙的办法能避免上面的错误。
理论上,我们只需要将 enable_if 的实例化推迟到 foo 的立即上下文中就行了,让 enable_if 的实例化发生在 foo 实例化的时候,而不是 S 实例化的时候。我们看下面这段代码:
- template <typename... Args>
- struct always_true : true_type {};
- template <typename T> struct S {
- template <typename U> static enable_if_t<always_true_v<U> && is_same_v<T, int>> foo(U) {}
- template <typename U> static enable_if_t<always_true_v<U> && !is_same_v<T, int>> foo(U) {}
- };
- S<int>::foo(1); // works
复制代码
我们实现一个无意义的 Metafunction,always_true,它接受任意的参数,然后返回 true。
然后,我们将 always_true_v<U> 放到 enable_if 的实参表达式里,U 是成员函数模板 foo 的形参,因为 always_true 永远是 true,所以这个合取表达式(&&)和原来的逻辑是一样的。
神奇,这样问题就解决了!
我们来解释下原理,在实例化 S<int> 时,编译器用 int 替换 T,第二个 foo 中的 !is_same_v<T, int> 就变为了 false。
但是对于这个合取表达式的剩余部分 always_true_v<U>,其值依赖于模板 foo 的形参 U,
而这个 U 直到 foo 实例化的时候编译器才能确定它的值,
所以编译器无法继续对 enable_if_t 求值了,所以也就没有任何非良构的表达式产生。
直到 foo(1) 开始进行实例化的时候,U 被推导为 int,always_true_v<U> 返回 true。
这时 enable_if_t<true && false> 就构成了非良构的,但是因为我们现在位于 foo 的立即上下文中,SFINAE!
另外,就像我们在 3.3.2 is_one_of 中提到的那样,模板的实例化是没有短路求值规则的,
所以这里即使把 always_true_v<U> 放在 !is_same_v<T, int> 的后面,也是可以把 enable_if 的实例化推迟到 foo 实例化之时的。
5.1.2 void_t
如果 enable_if 还没有让你大脑宕机,那试一下 void_t 吧!void_t 的定义如下:
- template <typename...> using void_t = void;
复制代码
看起来是一个很简单的东西,这就是一个别名模板,接受任意的类型形参,不论你传给它什么,它都返回给你一个 void。
这样一个东西能有啥用呢?说出来你可能不信,用它我们可以在 C++ 中实现一个类似 Python 里 hasattr 一样的东西。
假设我们现在想实现这样一个功能,要检查一个 Metafunction 是否遵守了 Metafunction Convention。
也就是说,给一个任意的类型,我们检查它内部是否定义了一个名为 “type” 的名字:
- // primary template
- template <typename, typename = void> struct has_type_member : false_type {};
- // partial specialization
- template <typename T> struct has_type_member<T, void_t<typename T::type>> : true_type {};
- std::cout << has_type_member_v<int> << std::endl; // 0, SFINAE
- std::cout << has_type_member_v<true_type> << std::endl; // 1
- std::cout << has_type_member_v<type_identity<int>> << std::endl; // 1
复制代码
我们定义了一个 Metafunction 叫做 has_type_member,
它的主模板接受两个参数,第一个参数是一个类型,第二个参数也是一个类型,但默认值置为了 void。
另有一个偏特化,偏特化保留了第一个参数 T,第二个参数期望匹配的是一个 void 类型,但我们没有直接写 void,而是写了 void_t<typename T::type>。
第一个关键点来了,我们先理解一下偏序规则是怎么在这里发挥作用的,
也就是说,什么情况下,会匹配 has_type_member 的偏特化。
对于第二个模板参数,当我们传入 void 时,由于主模板的能匹配任意的类型,而偏特化只匹配 void 类型,所以显然这时会匹配偏特化;
当我们给第二个参数传入非 void 时,由于偏特化不匹配非 void,所以这时会匹配主模板。
而重点是,当我们不传第二个参数时,那么编译器会从该形参的默认实参来确定实参,而默认实参就是 void。
也就是说,当我们不显示指定第二个实参时,模板的偏序规则会优先匹配 has_type_member 的偏特化。
第二个关键点是,在偏特化的第二个特化形参里,并不是直接指定了 void,而是指定的一个 void_t 实例化表达式,这个表达式是有可能是非良构的!
具体地说,当第一个形参 T 中包含名为 “type” 的成员、且 “type” 是一个类型时,表达式 typename T::type 良构;
而当第一个形参 T 中不包含 “type” 成员或 “type” 不是一个类型时,表达式 typename T::type 非良构。
这个非良构发生在 has_type_member 的形参列表里,属于立即上下文,因此 SFINAE 发挥作用。
也就是说,当 T 中不包含 “type” 类型时,偏特化被剔除,这次实例化只能匹配has_type_member 的主模板。
所以,我们使用 has_type_member 时,只指定一个参数,不指定第二个参数。
效果就是,当第一个参数里包含 “type” 成员时,匹配特化,结果为 true;
当第一个参数里不包含 “type” 成员时,匹配主模板,结果为 false。
最后需要说明一下的是,void_t 与 has_type_member 的实现与 void 没有直接的关系,void 不重要,重要的是第二个参数的默认实参要和 void_t 的返回类型匹配,从而触发偏序关系选择偏特化模板。只要遵守这个原理,你可以把 void 换成其他的类型,也一样能实现这个效果。
5.2 Example 5: Unevaluated Expressions
在 C++ 中,有四个运算符,它们的操作数是不会被求值(Evaluate)的,
因为这四个运算符只是对操作数的编译期属性进行访问。[7]
这四个运算符分别是:typeid, sizeof, noexcept, 以及decltype。
- std::size_t n = sizeof(std::cout << 42); // Nothing is outputed
- decltype(foo()) r = foo(); // Function foo is called only once.
复制代码
例如,第一句里的 std::cout << 42 并不会被执行,第二句中的 foo 也只调用了一次,
也就是说,这些运算符的操作数是不会在运行时生效的,它们甚至都不存在了,在编译期间就已经处理掉了。
这些运算符的表达式称为不求值表达式(Unevaluated Expression),它们的参数所处的区域称为不求值上下文(Unevaluated Context)。
5.2.1 declval
我们先来看不求值表达式的第一个用例,也是最简单的用例。加入我们定义了两个函数模板重载:
- template <typename T> enable_if_t<is_integral_v<T>, int> foo(T) {} // #1
- template <typename T> enable_if_t<is_floating_point_v<T>, float> foo(T) {} // #2
复制代码
第一个模板匹配整型的实参 T,接受一个 T 类型的变量作为函数参数,返回值类型为 int;
第二个模板匹配浮点数类型的实参 T,接受一个 T 类型的变量作为函数参数,返回值类型为 float。
然后,我们定义一个类模板 S,它内部有一个成员 value_,我们希望 value_ 的类型是 S 的形参 T 对应的 foo 重载的返回值类型:
- template <typename T> struct S { decltype(foo<T>(??)) value_; }; // What should be put in ??
复制代码
为了得到 foo 的返回值类型,我们只需要写一个 foo 的调用表达式,然后将这个表达式传入 decltype 运算符,就能得到 foo 的返回值类型。
并且根据我们知道 decltype 是一个不求值运算符,foo 的调用表达式并不会被求值。
但是问题在于,foo 函数接受一个类型为 T 的变量作为参数,我们去哪里创建一个 T 的变量出来呢?况且,在编译期也没变量啊。
巧妙的是,虽然我们不能真正地在编译期创建一个变量,但是在不求值表达式中,我们可以表示出一个“假想的”变量出来,通过 declval:
- // We can get a fictional variable by a function template named "declval"
- template <typename T> add_rvalue_reference_t<T> declval() noexcept; // only declaration, no definition!
复制代码
declval 是一个函数模板,首先我们注意到,它只有声明,没有定义。
因为在不求值上下文中,对这个模板的实例化不会被求值,我们不是真的要创建这个变量,只是要让编译器假设我们有一个这样的变量。
所以 declval 是不能被用在需要求值的地方的,它只能应用在不求值上下文中。
另外,它的返回值类型是 T 的右值引用,add_rvalue_reference 也是一个 Metafunction,它把接受 T,返回 T 的右值引用,
我们下节再介绍 add_rvalue_reference 的实现,现在只需要知道它的功能。
有了 declval,我们就可以伪造一个变量传给 foo 了:
- template <typename T> struct S { decltype(foo<T>(declval<T>())) value_; };
- std::cout << is_same_v<int, decltype(S<char>().value_)> << std::endl; // 1
- std::cout << is_same_v<float, decltype(S<double>().value_)> << std::endl; // 1
复制代码
declval<T>() 在不求值上下文中,就表示了一个 T 类型的变量。
5.2.2 add_lvalue_reference
结合不求值表达式和 SFINAE,我们能实现更复杂的功能。
还记得 remove_reference 吗?它移除一个类型的引用,现在我们反其道而行之,想实现两个 Metafunction,它们给类型加上引用。
特别地,add_lvalue_reference 给类型加上左值引用,add_rvalue_reference 给类型加上右值引用:
- template <typename T>
- struct add_lvalue_reference : type_identity<T&> {};
- template <typename T>
- struct add_rvalue_reference : type_identity<T&&> {};
复制代码
这两个 Metafunction 很简单,相信大家都能看懂,这段代码看起来似乎以为没什么问题。
等等,真的没问题吗?哦!如果我传入一个 void 会怎样?void 是没有相应的引用类型的,如果 T 是 void,那么将产生一个编译错误!
那怎么办呢,能不能想个办法,当 T 是 void 时就返回 void 本身,而不是尝试返回 void 的引用呢?
emmmm,对 void 进行特殊判断是一个办法,但是万一有别的类型也不支持添加引用呢?我们有一个更好的办法解决这个问题:
- namespace detail {
- template <typename T> type_identity<T&> try_add_lvalue_reference(int);
- template <typename T> type_identity<T> try_add_lvalue_reference(...);
- template <typename T> type_identity<T&&> try_add_rvalue_reference(int);
- template <typename T> type_identity<T> try_add_rvalue_reference(...);
- }
- template <typename T>
- struct add_lvalue_reference : decltype(detail::try_add_lvalue_reference<T>(0)) {};
- template <typename T>
- struct add_rvalue_reference : decltype(detail::try_add_rvalue_reference<T>(0)) {};
- std::cout << is_same_v<char&, add_lvalue_reference_t<char>> << std::endl; // 1
- std::cout << is_same_v<void, add_lvalue_reference_t<void>> << std::endl; // 1
复制代码
我们来解释一下这里的原理。
对 add_lvalue_reference,我们实现两个辅助函数模板:type_identity<T&> detail::try_add_lvalue_reference(int) 和 type_identity<T> detail::try_add_rvalue_reference(...)。
先看它们的返回值类型,他们的返回值类型一个是 type_identity<T&>, 一个是 type_identity<T>。
而这个返回值类型决定了 add_lvalue_reference 的结果,因为是直接继承过来的。而具体继承哪个呢?
取决于重载决议的结果。
当 T=void 时,type_identity<void&> 是一个非良构的表达式,根据 SFINAE,第一个辅助模板被剔除,所以 add_lvalue_reference 实际继承的是第二个辅助函数模板的返回值,也就是 type_identity<void>,返回 void。
而当 T=char 时,type_identity<char&> 是一个良构的表达式,这时两个函数模板都合法,那么根据偏序规则,由于第一个函数模板的函数形参类型特化程度更高,更匹配 detail::try_add_lvalue_reference<char>(0) 这个调用,所以第一个重载被选中,add_lvalue_reference 返回 char&。
可以看到,这种实现方式的思路是,不论 T 能否添加引用,我先假设能,先给它添上引用,反正也不求值,如果没错就最好,错了就 SFINAE。
所以这种实现方式是更通用的,如果除了 void 外,有其他类型也不能添加引用的,这个实现也能覆盖到。总之思路就是管他行不行,我先试试,不行再说。
5.2.3 is_copy_assignable
同样的思路,我们可以实现一个 Metafunction,来判断一个类型是不是可拷贝赋值的。类型 T 可以拷贝赋值的意思是,对两个 T 类型的变量 a 和 b,我们可以写表达式:a = b。
参照上面的思路,想知道能不能写 a = b,管他能不能写,我先写出来再说:
- template <typename T>
- using copy_assign_t = decltype(declval<T&>() = declval<T const&>());
复制代码
上面 decltype 括号里的就是一个赋值表达式,只不过我们是借助 declval 将它写出来的。
对于一个赋值表达式,等号左边是一个 T& 类型的变量,等号右边是一个 T const& 类型的变量(或者说常量?),并且赋值表达式的返回值类型等于等号左边变量的类型,也就是 T&。如果赋值这句话良构,那么 copy_assign_t 就等于 T&;如果非良构,那么我们只要保证它在立即上下文里就行了,就可以 SFINAE。
所以:
- template <typename T, typename = void> // default argument is essential
- struct is_copy_assignable : false_type {};
- template <typename T>
- struct is_copy_assignable<T, void_t<copy_assign_t<T>>> : true_type {};
- // S is not copy assignable
- struct S { S& operator=(S const&) = delete; };
- std::cout << is_copy_assignable_v<int> << std::endl; // 1
- std::cout << is_copy_assignable_v<true_type> << std::endl; // 1
- std::cout << is_copy_assignable_v<S> << std::endl; // 0
复制代码
我们把 copy_assign_t<T> 放到 void_t 里,剩下的事情就和你在 has_type_member 里看到的没有区别了。你可以自己尝试推演一下。
最后,你还能在标准库头文件 里找到更多相似的例子,你可以去探索探索。
5.3 Example 6: Applications in Real World
5.3.1 std::tuple
std::tuple 类似于 Python 的 tuple,可以存储多个不同类型的值。
在 Python 这种弱类型的语言中,tuple 并不难理解;但 C++ 是强类型的语言,不同类型的值是如何存储到一个列表的呢?
- int i = 1;
- auto t = std::tuple(0, i, '2', 3.0f, 4ll, std::string("five"));
- std::cout << std::get<1>(t) << std::endl; // output: 1
- std::cout << std::get<5>(t) << std::endl; // output: five
- 我们尝试实现一个自己的 tuple,来展示它的实现原理:
- // primary template
- template <typename... Args>
- struct tuple {
- // for class template arguments deduction
- tuple(Args...) {}
- };
- // partial specialization with recursive inheritance
- template <typename T, typename... Args>
- struct tuple<T, Args...> : tuple<Args...> {
- tuple(T v, Args... params) : value_(v), tuple<Args...>(params...) {}
- // value of T stores here
- T value_;
- };
复制代码
tuple 的主模板什么都不做,只是定义了一个构造函数,这个构造函数也没有任何逻辑,定义它是为了做类模板实参推导。
tuple 的模板特化实现了一个递归继承,tuple<T, Args...> 继承了 tuple<Args...>,这个递归继承会一直继承到 tuple<>,这时匹配主模板,递归终止。
特化模板定义了一个类型为 T 的成员 value_,也就是说,在递归继承的每一层都存储了一个 value,每一层的 value 的类型都可以是不同的,这就是 tuple 可以存储不同类型变量的关键。
如果你熟悉 C++ 的对象模型,那么你应该不难理解这些 value 是怎么存储的,它们在一块连续的内存空间内紧密排布,每一层根据其类型占据相应大小的空间。
了解了 tuple 的结构,我们再看下怎么读取 tuple 中的第 N 个元素,我们首先来看下如何获取 tuple 中第 N 个元素的类型:
- template <unsigned N, typename Tpl>
- struct tuple_element;
- template <unsigned N, typename T, typename... Args>
- struct tuple_element<N, tuple<T, Args...>>
- : tuple_element<N - 1, tuple<Args...>> {};
- template <typename T, typename... Args>
- struct tuple_element<0, tuple<T, Args...>> : type_identity<T> {
- using __tuple_type = tuple<T, Args...>;
- };
复制代码
和我们在 example 3.4.2: extent 中做的事情类似,tuple_element 也是对下标 N 做递归,直到 N=0,这时 T 就是第 N 个元素的类型。
但有一点点特别的是,这里除了 tuple_element<>::type 以外,我们还定义了一个 tuple_element<>::__tuple_type,它代表的是从 N 之后的参数组成的 tuple 类型。
例如,tuple_element<1, tuple<int, float, char>>::__tuple_type 就等于 tuple<float, char>。
- int i = 1;
- auto t = std::tuple(0, i, '2', 3.0f, 4ll, std::string("five"));
- std::cout << std::is_same_v<
- tuple_element<3, decltype(t)>::type,
- float> << std::endl; // output: 1
- std::cout << std::is_same_v<
- tuple_element<3, decltype(t)>::__tuple_type,
- tuple<float, long long, std::string>> << std::endl; // output: 1
复制代码
然后我们定义一个 get 函数,它直接通过一个类型转换就可以获得 tuple 的第 N 个元素。
- template <unsigned N, typename... Args>
- tuple_element_t<N, tuple<Args...>>& get(tuple<Args...>& t) {
- using __tuple_type = typename tuple_element<N, tuple<Args...>>::__tuple_type;
- return static_cast<__tuple_type&>(t).value_;
- }
复制代码
因为 tuple 是一层层继承的,所以这里对 t 相当于是一个向上转型,转型后直接返回这一层的 value 就行了。
另外 get 返回的是 value 的左值引用,也就是说 tuple 中的元素是可以修改的。
- int i = 1;
- auto t = std::tuple(0, i, '2', 3.0f, 4ll, std::string("five"));
- i = 0;
- std::cout << get<1>(t) << std::endl; // output: 1
- get<1>(t) = 0;
- std::cout << get<1>(t) << std::endl; // output: 0
复制代码
5.3.2 A Universal json::dumps
本节我们实现一个通用的 json::dumps,它支持将嵌套的 STL 容器序列化为 json 字符串,效果如下:
- auto li = std::list<int>{1, 2};
- auto tli = std::tuple(li, 3, "hello");
- auto mtli = std::map<std::string, decltype(tli)>{{"aaa", tli}, {"bbb", tli}};
- std::cout << json::dumps(mtli) << std::endl; // output: {"aaa":[[1, 2], 3, hello], "bbb":[[1, 2], 3, hello]}
复制代码
实现原理是使用函数模板重载的递归展开,并且通过 SFINAE 控制模板重载。在下面的实现中,我们优先使用 std 中的 Metafunction。
首先,对于内置的数值类型,我们直接转成 string 返回,这里通过 is_one_of 判定参数类型是不是内置的数值类型:
- namespace json {
- template <typename T>
- std::enable_if_t<
- is_one_of_v<
- std::decay_t<T>, // std::decay means remove reference, and remove cv
- int, long, long long, unsigned,
- unsigned long, unsigned long long,
- float, double, long double>,
- std::string>
- dumps(const T &value) {
- return std::to_string(value);
- }
- }
复制代码
std::decay 也是个 Metafunction,类似我们前面讲的 remove_reference,它返回 T 的原始类型,这样无论 T 是引用类型,还是 const/volatile,都能保证用原始类型和后面的参数进行比较。
通过 std::enable_if,当 dumps 参数的类型不是这些数值类型时,这个模板被从重载集中剔除。
然后对于 std::string 和其他的内置类型,可能要做一些特殊的处理:
- namespace json {
- // string, char
- template <typename T>
- std::enable_if_t<is_one_of_v<std::decay_t<T>, std::string, char>, std::string>
- dumps(const T &obj) {
- std::stringstream ss;
- ss << '"' << obj << '"';
- return ss.str();
- }
- // char *
- static inline std::string dumps(const char *s) {
- return json::dumps(std::string(s));
- }
- // void, nullptr
- template <typename T>
- std::
- enable_if_t<is_one_of_v<std::decay_t<T>, void, std::nullptr_t>, std::string>
- dumps(const T &) {
- return "null";
- }
- // bool
- template <typename T>
- std::enable_if_t<is_one_of_v<std::decay_t<T>, bool>, std::string>
- dumps(const T &value) {
- return value ? "true" : "false";
- }
- }
复制代码
下面就是重头戏了,对于 STL 中的容器,我们要递归地调用 dumps。
这里用到了 is_instantiation_of 来判定函数参数是不是容器实例:
- namespace json {
- // vector, list, deque, forward_list, set, multiset, unordered_set, unordered_multiset
- template <template <typename...> class Tmpl, typename... Args>
- std::enable_if_t<
- is_instantiation_of_v<Tmpl<Args...>, std::vector> ||
- is_instantiation_of_v<Tmpl<Args...>, std::list> ||
- is_instantiation_of_v<Tmpl<Args...>, std::deque> ||
- is_instantiation_of_v<Tmpl<Args...>, std::forward_list> ||
- is_instantiation_of_v<Tmpl<Args...>, std::set> ||
- is_instantiation_of_v<Tmpl<Args...>, std::multiset> ||
- is_instantiation_of_v<Tmpl<Args...>, std::unordered_set> ||
- is_instantiation_of_v<Tmpl<Args...>, std::unordered_multiset>,
- std::string>
- dumps(const Tmpl<Args...> &obj) {
- std::stringstream ss;
- ss << "[";
- for (auto itr = obj.begin(); itr != obj.end();) {
- ss << dumps(*itr);
- if (++itr != obj.end()) ss << ", ";
- }
- ss << "]";
- return ss.str();
- }
- // map, multimap, unordered_map, unordered_multimap
- template <template <typename...> class Tmpl, typename... Args>
- std::enable_if_t<
- is_instantiation_of_v<Tmpl<Args...>, std::map> ||
- is_instantiation_of_v<Tmpl<Args...>, std::multimap> ||
- is_instantiation_of_v<Tmpl<Args...>, std::unordered_map> ||
- is_instantiation_of_v<Tmpl<Args...>, std::unordered_multimap>,
- std::string>
- dumps(const Tmpl<Args...> &obj) {
- std::stringstream ss;
- ss << "{";
- for (auto itr = obj.begin(); itr != obj.end();) {
- ss << dumps(itr->first);
- ss << ":";
- ss << dumps(itr->second);
- if (++itr != obj.end()) ss << ", ";
- }
- ss << "}";
- return ss.str();
- }
- // std::pair
- template <typename T, typename U>
- std::string dumps(const std::pair<T, U> &obj) {
- std::stringstream ss;
- ss << "{" << dumps(obj.first) << ":" << dumps(obj.second) << "}";
- return ss.str();
- }
- }
复制代码
对于数组类型,使用到了我们前面提到的 extent,另外 std::is_array 可以判定 T 是不是一个数组。对于 std::array,直接可以通过模板参数获得数组的长度:
- namespace json {
- // array
- template <typename T>
- std::enable_if_t<std::is_array_v<T>, std::string> dumps(const T &arr) {
- std::stringstream ss;
- ss << "[";
- for (size_t i = 0; i < std::extent<T>::value; ++i) {
- ss << dumps(arr[i]);
- if (i != std::extent<T>::value - 1) ss << ", ";
- }
- ss << "]";
- return ss.str();
- }
- // std::array
- template <typename T, std::size_t N>
- std::string dumps(const std::array<T, N> &obj) {
- std::stringstream ss;
- ss << "[";
- for (auto itr = obj.begin(); itr != obj.end();) {
- ss << dumps(*itr);
- if (++itr != obj.end()) ss << ", ";
- }
- ss << "]";
- return ss.str();
- }
- }
复制代码
对于 std::tuple,由于它的实现原理比较特殊,所以逻辑与其他有一些不同,要写一个基于 tuple 长度 N 的递归展开:
- namespace json {
- // std::tuple
- template <size_t N, typename... Args>
- std::enable_if_t<N == sizeof...(Args) - 1, std::string>
- dumps(const std::tuple<Args...> &obj) {
- std::stringstream ss;
- ss << dumps(std::get<N>(obj)) << "]";
- return ss.str();
- }
- template <size_t N, typename... Args>
- std::enable_if_t<N != 0 && N != sizeof...(Args) - 1, std::string>
- dumps(const std::tuple<Args...> &obj) {
- std::stringstream ss;
- ss << dumps(std::get<N>(obj)) << ", " << dumps<N + 1, Args...>(obj);
- return ss.str();
- }
- template <size_t N = 0, typename... Args>
- std::enable_if_t<N == 0, std::string> dumps(const std::tuple<Args...> &obj) {
- std::stringstream ss;
- ss << "[" << dumps(std::get<N>(obj)) << ", " << dumps<N + 1, Args...>(obj);
- return ss.str();
- }
- }
复制代码
对于指针类型,我们可能希望输出它指向的值:
- namespace json {
- // pointer
- template <typename T>
- std::string dumps(const T *p) {
- return dumps(*p);
- }
- // shared_ptr, weak_ptr, unique_ptr
- template <typename T>
- std::enable_if_t<
- is_instantiation_of_v<T, std::shared_ptr> ||
- is_instantiation_of_v<T, std::weak_ptr> ||
- is_instantiation_of_v<T, std::unique_ptr>,
- std::string>
- dumps(const std::shared_ptr<T> &p) {
- return dumps(*p);
- }
- }
复制代码
我们定义了很多模板,但这时我们会遇到第一个问题就是名字查找的问题。
如果我们直接把上面这些模板的定义写到头文件里,由于这些函数模板是相互调用的,例如 json::dumps<std::list<std::map<..., std::list<int>>>>(...) 会先调用 list 对应的 dumps,
再调用 map 对应的 dumps,
再调用 list 对应的 dumps,
最后调用 int 对应的 dumps;
所以这些模板之间的名字查找就存在了顺序依赖。
比如 int 对应的 dumps 必须放在 list 对应的 dumps 之前,以保证 dumps<list> 能查找到 dumps<int>。
但 list 和 map 在这个例子中是相互依赖的,把谁放前面都不行。
所以,我们必须先前向声明(Forward Declare)所有的 dumps 模板。
这和函数的前向声明是同一个道理,如果你熟悉名字查找问题的话应该不难理解。
所以虽然我们在 Inclusion Model 中把模板的定义直接放在头文件里,但是模板的声明在某些情况下也是必不可少的。
我们已经处理了内置类型和 STL 中的类型,这时我们遇到的第二个问题就是,对于用户的自定义类型,我们应该怎么处理?
比如一个 vector 里存了一个用户自定义的类型,json::dumps<vector<UserDefine>> 展开以后,怎么处理 UserDefine 的对象?
这里就要用到依赖于实参的名字查找(Argument-dependent Name Lookup, ADL),
ADL 是指,编译器会去函数实参类型所在的名字空间里去查找函数名字。
所以,我们在定义 UserDefine 类型时,在同一个名字空间内提供一个针对 UserDefine 的 json::dumps 重载即可。
这样,我们在 json.hpp 里定义的对应 vector 参数的函数模板,就也能查找到 UserDefine 的重载了。
- namespace user_namespace {
- struct UserDefine { int a; };
- std::string dumps(const UserDefine &obj) {
- return "ud" + std::to_string(obj.a);
- }
- } // namespace user_namespace
- using user_namespace::UserDefine;
- int main() {
- auto vu = std::vector<UserDefine>{UserDefine{1}, UserDefine{1}};
- auto li = std::list<int>{1, 2};
- auto tvuli = std::tuple(vu, li, 3, "hello");
- auto mtvuli = std::map<std::string, decltype(tvuli)>{{"aaa", tvuli}, {"bbb", tvuli}};
- std::cout << json::dumps(mtvuli) << std::endl;
- // output: {"aaa":[[ud1, ud1], [1, 2], 3, "hello"], "bbb":[[ud1, ud1], [1, 2], 3, "hello"]}
- }
复制代码 |
|