.. TODO 类型萃取、类型擦除、完美转发 模板 ######################################## 模板作为 C++ 支持的第四种范程,最初与类相同,只是为了减少代码的编写。但如今,C++ 的模板元编程( TMP )已经成为了一种 ``图灵完备`` 的语言。模板编程与普通的函数式范程和面向对象范程相同,同时具备条件判断、循环、多态的特点。模板具有以下特点: - 将程序从运行期提前到编译器 - 隐式接口和编译期多态 - 图灵完备的 TMP (Template Meta Programmer) .. note:: - 需要注意的是, TMP 与普通的模板编程应当区分开,TMP 意味着代码在编译期执行完毕,运行期只负责输出结果;而普通的模板编程只是用来重用代码 - 某些错误的代码可能依然能够通过编译,这是因为如果计算结果不通过执行代码就可以在编译期分析出来,那么代码实际上并不会执行 [#error]_ .. [#error] `模板的生成时机 `_ 模板介绍 **************************************** 模板由关键字 ``template`` 和 ``typename`` 声明,与普通声明不同的是,``模板声明的是类型而不是变量``。从而达到 “一次声明,多类型使用” 的目的。将不必要的工作移交给编译器处理,从而大大减少了代码工作量。 一个典型的模板声明与使用如下: .. code-block:: cpp template void output(T a, N b){ cout<(10, 10.f); output(20, "hello, world"); return 0; } .. note:: 在面向对象中,一般我们习惯将函数的声明和实现进行分离,而在模板中,则需要将函数的声明和实现写在同一个文件中 .. admonition:: 什么是谓词? 谓词就是返回值为真或者假的函数。STL 容器中经常会使用到谓词,用于模板参数。 [#]_ .. [#] `STL 容器简介 - OI Wiki `_ 模板参数默认值 ======================================== 与函数参数的默认值相同,你可以为模板参数指定默认类型。但不同的是,模板参数的默认值只是用来帮助编译器推导参数类型,而且对参数右边的参数不做要求,比如: .. code-block:: cpp template void output(T a, N b){ cout<``:适用于 ``相同类型,不同值`` 的特化 例如: .. code-block:: cpp // 类型特化 template void output(T a){ cout< void output(){ cout< void output<1>(){ cout<<"[1]"< void doProcessing(T& w){ if(w.size() > 10 && w!= someNastyWidget){ T temp(w); temp.normalize(); temp.swap(); } } 从表面上来看,类型 T 必须拥有: - size, normalize, swap 成员函数,拷贝构造函数,可用于 someNastyWidget 的 operator> 函数,则就是所谓的 ``隐式接口`` - 以不同的参数对函数进行调用将会导致编译器生成不同的函数,这就是所谓的 ``编译期多态`` 更近一步地,通过 ``typename`` 的第二种语法,我们可以拓展隐式接口的范围: [#E42]_ .. code-block:: cpp template void workWithIterator(IterT iter){ typename iterator_traits::value_type temp(*iter); cout<{ using value_type = remove_cv_t<_Tp>; using difference_type = ptrdiff_t; using pointer = _Tp*; using reference = _Tp&; }; .. note:: 在 C++ 20 中,可以通过 ``concepts`` 将模板参数需要满足的隐式接口进行抽离,而不是分散到整个函数中 .. [#E41] Effective C++ 第三版条款41:了解隐式接口和编译期多态 .. [#E42] Effective C++ 第三版条款42:了解 typename 的双重意义 条件判断和循环 ======================================== 要实现条件判断,可以通过模板的特化来做到: 首先实现相等判断: [#]_ .. code-block:: cpp template struct IS_SAME{ enum{ result = 0 }; }; template struct IS_SAME{ enum { result = 1}; }; 现在,当用相同类型调用 ``IS_SAME`` 是,其 result = 1, 当用不同类型调用 ``IS_SAME`` 时,其 result = 0。 然后实现条件语句: .. code-block:: cpp template struct if_{ typedef Type1 return_type; }; template struct if_{ typedef Type2 return_type; }; 现在,当 ``if_`` 的第一个参数为 true 是,返回第二个模板参数的类型,当 ``if_`` 的第一个参数为 false 时,返回第三个模板参数的类型: .. code-block:: cpp typename if_< IS_SAME::result, int, char>::return_type a; 可见,``模板中的条件判断实现的是类型判断和类型选择`` 与普通的循环不同的是,模板中的循环是通过对函数进行 ``递归`` 实现的: .. code-block:: cpp template void output() { output(); cout << i << " "; } template<> void output<1>(){ cout<<1<<" "; } int main() { output<10>(); return 0; } 显然,由于需要在模板特化时指定具体值,``模板循环只适用于同一种类型`` .. [#] 深入实践 C++ 模板编程(温宇杰):13.2 元函数 可变长模板参数 ======================================== C++11引入了名为 ``参数包`` 的可变长模板参数,其声明方式和限制如下: - 以 ```...`` 开头的参数为参数包 - 参数包必须位于模板参数末尾 下面提供了展开参数包的方法 #. 使用递归在函数中展开实参参数包: .. code-block:: cpp template void output(T a){ cout< void output(T a, Args ...args){ cout< class Derived: public Bases...{ // 在基类列表展开模板参数 Derived(const Bases... bases) : Bases(bases)...{ // 在基类初始化列表中展开 } }; template struct count { static const std::size_t value = sizeof...(Types); // 使用 size...展开包 }; template void func() throw(exceptions...){ // 动态异常展开 C++17已废弃 } #. 通过折叠表达式展开参数包 [#]_ 自 C++17,添加了折叠表达式用于更方便地展开参数包: .. code-block:: cpp template double sum(Args ...args){ return (args += ...); } 当然,你依然需要注意精度的问题。 .. [#] `形参包 `_ .. [#] ISO/IEC 14882 14.5.3 Variadic templates .. [#] `折叠表达式 `_ 函数返回值类型提取 ======================================== 在某些情况下,使用模板可能会导致精度丢失: .. code-block:: template T sum(T a){ return a; } template T sum(T a, Args ...args){ return a + sum(args...); } 现在我们通过一系列巧妙的设置,这样调用该函数:``cout< struct higest_precision{ typedef typename higest_precision::type T2; // 将 nullptr 转换为两种类型的指针,然后再解引用 typedef decltype(*((T1*)nullptr) + *((T2*) nullptr)) type; }; template struct higest_precision{ typedef T type; }; template T sum(T a){ return a; } template typename higest_precision::type sum(T a, Args ...args){ return a + sum(args...); } 对于没有使用参数包的模板来说,可以使用 ``auto + decltype`` 这种更加简单的方式: .. code-block:: cpp template auto sum(T1 a, T2 b, T3 c) -> decltype(a + b + c){ return a + b + c; } 这种方式被称为 ``函数后置返回类型`` .. [#] 深入实践 C++ 模板编程(温宇杰):例 15.10 奇异递归模板 **************************************** 奇异递归模板 ( :abbr:`CRTP (Curiously Recurring Template Pattern)` ) 使得父类可以在编译器感知到子类的存在。主要用于: - 静态多态 在使用接口规范子类时,大量运行时的类型转换会导致极大的开销,使用 CRTP 可以在编译期就获取子类,通过 static_cast 获取子类指针就避免了 dynamic_cast 的开销 .. code-block:: cpp template class Bird{ public: T getBird(){ // 静态多态 return static_cast(*this); } }; class Chicken: public Bird{ }; template const char* bird_name(Bird& bird){ return typeid(bird.getBird()).name(); } int main(){ Chicken ch; std::cout<`_ 完美转发 **************************************** .. admonition:: 注 整理自 `C++新标准002_动动小手指就能实现 " 完美转发 " `_ 假设我们有个函数 factory,其功能是将传入的参数包装成了一个 shared_ptr。一个简单的实现如下: .. code-block:: cpp template std::shared_ptr factory(Arg arg) { return std::shared_ptr(new T(arg)); } 但问题是 arg 会引起一次拷贝,为了性能考虑,可以将参数类型更改为 T&,但是这样就没法匹配到右值了。另一个方式是将参数类型更改为 const T&,但是这样函数内又没法对 arg 做处理了。 另一种方式是分别为左值和右值分别写一个函数。但是无疑十分麻烦。 完美转发就因此而生。完美转发的核心概念为引用折叠 引用折叠的规则可以简要概括为: .. code-block:: none T& & -> T& T&& & --> T& T& && -> T& T&& && -> T&& 值得注意的是 T&&,其完美保留了参数的实际类型,因此又被称为完美引用。完美引用可以同时匹配左值和右值。 借助完美引用和 std\:\:forward,我们可以写出一个简单形式的函数: .. code-block:: cpp template std::shared_ptr factory(Arg&& arg) { return std::shared_ptr(new T(std::forward(arg))); } 调用的方式为: .. code-block:: cpp auto ptr = factory(10); int a = 20; auto ptr2 = factory(a);