请选择 进入手机版 | 继续访问电脑版

C++模板元编程(五):从进阶的案例中学习

发表于 2023-1-23 22:12:12 显示全部楼层 0 391

5. Learn TMP in use (Part II)
5.1 Example 4: SFINAE

本节展示两个基于 SFINAE 的例子,enable_if 和 void_t。

5.1.1 enable_if
假设我们想要这样的函数模板重载,对于整型的参数,匹配第一个模板,这个模板实现一些针对整型参数的逻辑;
对于浮点数型的参数,匹配第二个模板,这个模板实现针对浮点型参数的逻辑。

于是我们定义了下面两个函数模板重载:


  1. // The first one is for integral type such as int, char, long...
  2. template <typename INT> void foo(INT) {}
  3. // The second one is for floating point types such as float, double...
  4. template <typename FLT> void foo(FLT) {}    // Error: redefinition!
复制代码


但是这时我们发现,编译器报告了一个重定义错误,上面两个模板实际上是一模一样的,因为它们的签名完全相同。
我们无法写一个针对参数类型的函数模板重载。唯一的办法是对每一个类型,int, char, long, float, double... 都写一个普通函数重载:


  1. void foo(int)   {}
  2. void foo(char)  {}
  3. void foo(float) {}
  4. // ...
复制代码


但是这样我们要定义十几个相似的函数,完全失去了泛型编程的支持。这时,我们就需要 enable_if 出马了:


  1. template <bool, typename T = void>
  2. struct enable_if : type_identity<T> {};

  3. template <typename T>
  4. struct enable_if<false, T> {};

  5. template <typename T> enable_if_t<is_integral_v<T>>       foo(T) {}  // #1
  6. template <typename T> enable_if_t<is_floating_point_v<T>> foo(T) {}  // #2
  7. foo(1);     // match #1
  8. 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 的威力。

我们来看第二个例子,假设我们想通过类模板的形参来控制类模板成员函数的重载:


  1. template <typename T> struct S {
  2.   template <typename U> static enable_if_t< is_same_v<T, int>> foo(U) {}    // #1
  3.   template <typename U> static enable_if_t<!is_same_v<T, int>> foo(U) {}    // #2
  4. };
  5. S<int>::foo(1);
复制代码


在这里,foo 是 S 的静态成员函数模板,我们希望,当 S 的实参是 int 时,匹配第一个 foo;
当 S 的实参不是 int 时,匹配第二个 foo。也就是说,我们希望 S<int>::foo(1) 调用的是 #1。
但是,我们去编译这段代码却发现编译器报告了错误:


  1. metaprogramming/immediate_context.cpp:44:40:
  2.     error: no type named 'type' in 'enable_if<false>';
  3.     'enable_if' cannot be used to disable this declaration
  4.     using enable_if_t = typename enable_if<B, T>::type;
  5.                                        ^
  6. metaprogramming/immediate_context.cpp:52:10:
  7.     note: in instantiation of template type alias 'enable_if_t' requested here
  8.     static enable_if_t<!is_same_v<int, T>> foo(U) {}
  9.            ^
  10. metaprogramming/immediate_context.cpp:55:14:
  11.     note: in instantiation of template class 'S<int>' requested here
  12.     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 实例化的时候。我们看下面这段代码:



  1. template <typename... Args>
  2. struct always_true : true_type {};

  3. template <typename T> struct S {
  4.   template <typename U> static enable_if_t<always_true_v<U> &&  is_same_v<T, int>> foo(U) {}
  5.   template <typename U> static enable_if_t<always_true_v<U> && !is_same_v<T, int>> foo(U) {}
  6. };

  7. 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 的定义如下:


  1. template <typename...> using void_t = void;
复制代码


看起来是一个很简单的东西,这就是一个别名模板,接受任意的类型形参,不论你传给它什么,它都返回给你一个 void。
这样一个东西能有啥用呢?说出来你可能不信,用它我们可以在 C++ 中实现一个类似 Python 里 hasattr 一样的东西。

假设我们现在想实现这样一个功能,要检查一个 Metafunction 是否遵守了 Metafunction Convention。
也就是说,给一个任意的类型,我们检查它内部是否定义了一个名为 “type” 的名字:


  1. // primary template
  2. template <typename, typename = void> struct has_type_member : false_type {};
  3. // partial specialization
  4. template <typename T> struct has_type_member<T, void_t<typename T::type>> : true_type {};

  5. std::cout << has_type_member_v<int> << std::endl;                // 0, SFINAE
  6. std::cout << has_type_member_v<true_type> << std::endl;          // 1
  7. 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。


  1. std::size_t n = sizeof(std::cout << 42);  // Nothing is outputed
  2. decltype(foo()) r = foo();                // Function foo is called only once.
复制代码


例如,第一句里的 std::cout << 42 并不会被执行,第二句中的 foo 也只调用了一次,
也就是说,这些运算符的操作数是不会在运行时生效的,它们甚至都不存在了,在编译期间就已经处理掉了。
这些运算符的表达式称为不求值表达式(Unevaluated Expression),它们的参数所处的区域称为不求值上下文(Unevaluated Context)。

5.2.1 declval
我们先来看不求值表达式的第一个用例,也是最简单的用例。加入我们定义了两个函数模板重载:


  1. template <typename T> enable_if_t<is_integral_v<T>,       int>   foo(T) {}  // #1
  2. 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 重载的返回值类型:


  1. template <typename T> struct S { decltype(foo<T>(??)) value_; };  // What should be put in ??
复制代码


为了得到 foo 的返回值类型,我们只需要写一个 foo 的调用表达式,然后将这个表达式传入 decltype 运算符,就能得到 foo 的返回值类型。
并且根据我们知道 decltype 是一个不求值运算符,foo 的调用表达式并不会被求值。
但是问题在于,foo 函数接受一个类型为 T 的变量作为参数,我们去哪里创建一个 T 的变量出来呢?况且,在编译期也没变量啊。

巧妙的是,虽然我们不能真正地在编译期创建一个变量,但是在不求值表达式中,我们可以表示出一个“假想的”变量出来,通过 declval:


  1. // We can get a fictional variable by a function template named "declval"
  2. 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 了:

  1. template <typename T> struct S { decltype(foo<T>(declval<T>())) value_; };

  2. std::cout << is_same_v<int,   decltype(S<char>().value_)>   << std::endl;    // 1
  3. 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 给类型加上右值引用:


  1. template <typename T>
  2. struct add_lvalue_reference : type_identity<T&> {};


  3. template <typename T>
  4. struct add_rvalue_reference : type_identity<T&&> {};
复制代码


这两个 Metafunction 很简单,相信大家都能看懂,这段代码看起来似乎以为没什么问题。
等等,真的没问题吗?哦!如果我传入一个 void 会怎样?void 是没有相应的引用类型的,如果 T 是 void,那么将产生一个编译错误!
那怎么办呢,能不能想个办法,当 T 是 void 时就返回 void 本身,而不是尝试返回 void 的引用呢?
emmmm,对 void 进行特殊判断是一个办法,但是万一有别的类型也不支持添加引用呢?我们有一个更好的办法解决这个问题:


  1. namespace detail {

  2.   template <typename T> type_identity<T&> try_add_lvalue_reference(int);
  3.   template <typename T> type_identity<T>  try_add_lvalue_reference(...);

  4.   template <typename T> type_identity<T&&> try_add_rvalue_reference(int);
  5.   template <typename T> type_identity<T>   try_add_rvalue_reference(...);
  6. }

  7. template <typename T>
  8. struct add_lvalue_reference : decltype(detail::try_add_lvalue_reference<T>(0)) {};
  9. template <typename T>
  10. struct add_rvalue_reference : decltype(detail::try_add_rvalue_reference<T>(0)) {};

  11. std::cout << is_same_v<char&, add_lvalue_reference_t<char>> << std::endl;    // 1
  12. 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,管他能不能写,我先写出来再说:


  1. template <typename T>
  2. using copy_assign_t = decltype(declval<T&>() = declval<T const&>());
复制代码


上面 decltype 括号里的就是一个赋值表达式,只不过我们是借助 declval 将它写出来的。
对于一个赋值表达式,等号左边是一个 T& 类型的变量,等号右边是一个 T const& 类型的变量(或者说常量?),并且赋值表达式的返回值类型等于等号左边变量的类型,也就是 T&。如果赋值这句话良构,那么 copy_assign_t 就等于 T&;如果非良构,那么我们只要保证它在立即上下文里就行了,就可以 SFINAE。

所以:


  1. template <typename T, typename = void>      // default argument is essential
  2. struct is_copy_assignable : false_type {};

  3. template <typename T>
  4. struct is_copy_assignable<T, void_t<copy_assign_t<T>>> : true_type {};

  5. // S is not copy assignable
  6. struct S { S& operator=(S const&) = delete; };
  7. std::cout << is_copy_assignable_v<int> << std::endl;        // 1
  8. std::cout << is_copy_assignable_v<true_type> << std::endl;  // 1
  9. 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++ 是强类型的语言,不同类型的值是如何存储到一个列表的呢?


  1. int i = 1;
  2. auto t = std::tuple(0, i, '2', 3.0f, 4ll, std::string("five"));

  3. std::cout << std::get<1>(t) << std::endl;       // output: 1
  4. std::cout << std::get<5>(t) << std::endl;       // output: five
  5. 我们尝试实现一个自己的 tuple,来展示它的实现原理:

  6. // primary template
  7. template <typename... Args>
  8. struct tuple {
  9.   // for class template arguments deduction
  10.   tuple(Args...) {}
  11. };

  12. // partial specialization with recursive inheritance
  13. template <typename T, typename... Args>
  14. struct tuple<T, Args...> : tuple<Args...> {
  15.   tuple(T v, Args... params) : value_(v), tuple<Args...>(params...) {}
  16.   // value of T stores here
  17.   T value_;
  18. };
复制代码


tuple 的主模板什么都不做,只是定义了一个构造函数,这个构造函数也没有任何逻辑,定义它是为了做类模板实参推导。
tuple 的模板特化实现了一个递归继承,tuple<T, Args...> 继承了 tuple<Args...>,这个递归继承会一直继承到 tuple<>,这时匹配主模板,递归终止。
特化模板定义了一个类型为 T 的成员 value_,也就是说,在递归继承的每一层都存储了一个 value,每一层的 value 的类型都可以是不同的,这就是 tuple 可以存储不同类型变量的关键。
如果你熟悉 C++ 的对象模型,那么你应该不难理解这些 value 是怎么存储的,它们在一块连续的内存空间内紧密排布,每一层根据其类型占据相应大小的空间。

tuple.jpg

了解了 tuple 的结构,我们再看下怎么读取 tuple 中的第 N 个元素,我们首先来看下如何获取 tuple 中第 N 个元素的类型:


  1. template <unsigned N, typename Tpl>
  2. struct tuple_element;

  3. template <unsigned N, typename T, typename... Args>
  4. struct tuple_element<N, tuple<T, Args...>>
  5.     : tuple_element<N - 1, tuple<Args...>> {};

  6. template <typename T, typename... Args>
  7. struct tuple_element<0, tuple<T, Args...>> : type_identity<T> {
  8.   using __tuple_type = tuple<T, Args...>;
  9. };
复制代码


和我们在 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>。


  1. int i = 1;
  2. auto t = std::tuple(0, i, '2', 3.0f, 4ll, std::string("five"));
  3. std::cout << std::is_same_v<
  4.                tuple_element<3, decltype(t)>::type,
  5.                float> << std::endl;                                 // output: 1
  6. std::cout << std::is_same_v<
  7.                tuple_element<3, decltype(t)>::__tuple_type,
  8.                tuple<float, long long, std::string>> << std::endl;  // output: 1
复制代码


然后我们定义一个 get 函数,它直接通过一个类型转换就可以获得 tuple 的第 N 个元素。


  1. template <unsigned N, typename... Args>
  2. tuple_element_t<N, tuple<Args...>>& get(tuple<Args...>& t) {
  3.   using __tuple_type = typename tuple_element<N, tuple<Args...>>::__tuple_type;
  4.   return static_cast<__tuple_type&>(t).value_;
  5. }
复制代码


因为 tuple 是一层层继承的,所以这里对 t 相当于是一个向上转型,转型后直接返回这一层的 value 就行了。
另外 get 返回的是 value 的左值引用,也就是说 tuple 中的元素是可以修改的。


  1. int i = 1;
  2. auto t = std::tuple(0, i, '2', 3.0f, 4ll, std::string("five"));
  3. i = 0;
  4. std::cout << get<1>(t) << std::endl;  // output: 1
  5. get<1>(t) = 0;
  6. std::cout << get<1>(t) << std::endl;  // output: 0
复制代码


5.3.2 A Universal json::dumps

本节我们实现一个通用的 json::dumps,它支持将嵌套的 STL 容器序列化为 json 字符串,效果如下:


  1. auto li = std::list<int>{1, 2};
  2. auto tli = std::tuple(li, 3, "hello");
  3. auto mtli = std::map<std::string, decltype(tli)>{{"aaa", tli}, {"bbb", tli}};

  4. 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 判定参数类型是不是内置的数值类型:


  1. namespace json {
  2. template <typename T>
  3. std::enable_if_t<
  4.     is_one_of_v<
  5.         std::decay_t<T>,    // std::decay means remove reference, and remove cv
  6.         int, long, long long, unsigned,
  7.         unsigned long, unsigned long long,
  8.         float, double, long double>,
  9.     std::string>
  10. dumps(const T &value) {
  11.   return std::to_string(value);
  12. }
  13. }
复制代码


std::decay 也是个 Metafunction,类似我们前面讲的 remove_reference,它返回 T 的原始类型,这样无论 T 是引用类型,还是 const/volatile,都能保证用原始类型和后面的参数进行比较。
通过 std::enable_if,当 dumps 参数的类型不是这些数值类型时,这个模板被从重载集中剔除。

然后对于 std::string 和其他的内置类型,可能要做一些特殊的处理:


  1. namespace json {
  2. // string, char
  3. template <typename T>
  4. std::enable_if_t<is_one_of_v<std::decay_t<T>, std::string, char>, std::string>
  5. dumps(const T &obj) {
  6.   std::stringstream ss;
  7.   ss << '"' << obj << '"';
  8.   return ss.str();
  9. }

  10. // char *
  11. static inline std::string dumps(const char *s) {
  12.   return json::dumps(std::string(s));
  13. }

  14. // void, nullptr
  15. template <typename T>
  16. std::
  17.     enable_if_t<is_one_of_v<std::decay_t<T>, void, std::nullptr_t>, std::string>
  18.     dumps(const T &) {
  19.   return "null";
  20. }

  21. // bool
  22. template <typename T>
  23. std::enable_if_t<is_one_of_v<std::decay_t<T>, bool>, std::string>
  24. dumps(const T &value) {
  25.   return value ? "true" : "false";
  26. }
  27. }
复制代码


下面就是重头戏了,对于 STL 中的容器,我们要递归地调用 dumps。
这里用到了 is_instantiation_of 来判定函数参数是不是容器实例:


  1. namespace json {
  2. // vector, list, deque, forward_list, set, multiset, unordered_set, unordered_multiset
  3. template <template <typename...> class Tmpl, typename... Args>
  4. std::enable_if_t<
  5.         is_instantiation_of_v<Tmpl<Args...>, std::vector> ||
  6.         is_instantiation_of_v<Tmpl<Args...>, std::list> ||
  7.         is_instantiation_of_v<Tmpl<Args...>, std::deque> ||
  8.         is_instantiation_of_v<Tmpl<Args...>, std::forward_list> ||
  9.         is_instantiation_of_v<Tmpl<Args...>, std::set> ||
  10.         is_instantiation_of_v<Tmpl<Args...>, std::multiset> ||
  11.         is_instantiation_of_v<Tmpl<Args...>, std::unordered_set> ||
  12.         is_instantiation_of_v<Tmpl<Args...>, std::unordered_multiset>,
  13.     std::string>
  14. dumps(const Tmpl<Args...> &obj) {
  15.   std::stringstream ss;
  16.   ss << "[";
  17.   for (auto itr = obj.begin(); itr != obj.end();) {
  18.     ss << dumps(*itr);
  19.     if (++itr != obj.end()) ss << ", ";
  20.   }
  21.   ss << "]";
  22.   return ss.str();
  23. }

  24. // map, multimap, unordered_map, unordered_multimap
  25. template <template <typename...> class Tmpl, typename... Args>
  26. std::enable_if_t<
  27.         is_instantiation_of_v<Tmpl<Args...>, std::map> ||
  28.         is_instantiation_of_v<Tmpl<Args...>, std::multimap> ||
  29.         is_instantiation_of_v<Tmpl<Args...>, std::unordered_map> ||
  30.         is_instantiation_of_v<Tmpl<Args...>, std::unordered_multimap>,
  31.     std::string>
  32. dumps(const Tmpl<Args...> &obj) {
  33.   std::stringstream ss;
  34.   ss << "{";
  35.   for (auto itr = obj.begin(); itr != obj.end();) {
  36.     ss << dumps(itr->first);
  37.     ss << ":";
  38.     ss << dumps(itr->second);
  39.     if (++itr != obj.end()) ss << ", ";
  40.   }
  41.   ss << "}";
  42.   return ss.str();
  43. }

  44. // std::pair
  45. template <typename T, typename U>
  46. std::string dumps(const std::pair<T, U> &obj) {
  47.   std::stringstream ss;
  48.   ss << "{" << dumps(obj.first) << ":" << dumps(obj.second) << "}";
  49.   return ss.str();
  50. }
  51. }
复制代码


对于数组类型,使用到了我们前面提到的 extent,另外 std::is_array 可以判定 T 是不是一个数组。对于 std::array,直接可以通过模板参数获得数组的长度:


  1. namespace json {
  2. // array
  3. template <typename T>
  4. std::enable_if_t<std::is_array_v<T>, std::string> dumps(const T &arr) {
  5.   std::stringstream ss;
  6.   ss << "[";
  7.   for (size_t i = 0; i < std::extent<T>::value; ++i) {
  8.     ss << dumps(arr[i]);
  9.     if (i != std::extent<T>::value - 1) ss << ", ";
  10.   }
  11.   ss << "]";
  12.   return ss.str();
  13. }

  14. // std::array
  15. template <typename T, std::size_t N>
  16. std::string dumps(const std::array<T, N> &obj) {
  17.   std::stringstream ss;
  18.   ss << "[";
  19.   for (auto itr = obj.begin(); itr != obj.end();) {
  20.     ss << dumps(*itr);
  21.     if (++itr != obj.end()) ss << ", ";
  22.   }
  23.   ss << "]";
  24.   return ss.str();
  25. }
  26. }
复制代码


对于 std::tuple,由于它的实现原理比较特殊,所以逻辑与其他有一些不同,要写一个基于 tuple 长度 N 的递归展开:


  1. namespace json {
  2. // std::tuple
  3. template <size_t N, typename... Args>
  4. std::enable_if_t<N == sizeof...(Args) - 1, std::string>
  5. dumps(const std::tuple<Args...> &obj) {
  6.   std::stringstream ss;
  7.   ss << dumps(std::get<N>(obj)) << "]";
  8.   return ss.str();
  9. }
  10. template <size_t N, typename... Args>
  11. std::enable_if_t<N != 0 && N != sizeof...(Args) - 1, std::string>
  12. dumps(const std::tuple<Args...> &obj) {
  13.   std::stringstream ss;
  14.   ss << dumps(std::get<N>(obj)) << ", " << dumps<N + 1, Args...>(obj);
  15.   return ss.str();
  16. }
  17. template <size_t N = 0, typename... Args>
  18. std::enable_if_t<N == 0, std::string> dumps(const std::tuple<Args...> &obj) {
  19.   std::stringstream ss;
  20.   ss << "[" << dumps(std::get<N>(obj)) << ", " << dumps<N + 1, Args...>(obj);
  21.   return ss.str();
  22. }
  23. }
复制代码


对于指针类型,我们可能希望输出它指向的值:


  1. namespace json {
  2. // pointer
  3. template <typename T>
  4. std::string dumps(const T *p) {
  5.   return dumps(*p);
  6. }
  7. // shared_ptr, weak_ptr, unique_ptr
  8. template <typename T>
  9. std::enable_if_t<
  10.     is_instantiation_of_v<T, std::shared_ptr> ||
  11.         is_instantiation_of_v<T, std::weak_ptr> ||
  12.         is_instantiation_of_v<T, std::unique_ptr>,
  13.     std::string>
  14. dumps(const std::shared_ptr<T> &p) {
  15.   return dumps(*p);
  16. }
  17. }
复制代码


我们定义了很多模板,但这时我们会遇到第一个问题就是名字查找的问题。
如果我们直接把上面这些模板的定义写到头文件里,由于这些函数模板是相互调用的,例如 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 的重载了。


  1. namespace user_namespace {

  2. struct UserDefine { int a; };

  3. std::string dumps(const UserDefine &obj) {
  4.   return "ud" + std::to_string(obj.a);
  5. }

  6. }  // namespace user_namespace

  7. using user_namespace::UserDefine;

  8. int main() {
  9.   auto vu = std::vector<UserDefine>{UserDefine{1}, UserDefine{1}};
  10.   auto li = std::list<int>{1, 2};
  11.   auto tvuli = std::tuple(vu, li, 3, "hello");
  12.   auto mtvuli = std::map<std::string, decltype(tvuli)>{{"aaa", tvuli}, {"bbb", tvuli}};
  13.   std::cout << json::dumps(mtvuli) << std::endl;
  14.   // output: {"aaa":[[ud1, ud1], [1, 2], 3, "hello"], "bbb":[[ud1, ud1], [1, 2], 3, "hello"]}
  15. }
复制代码
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

shop_da_admin

管理员

39

主题

40

帖子

247

积分
Ta的主页 发消息

网友分享更多 >

  • 机器学习的统计学知识
  • 漳州盛泰水产
  • 玉川茶家