当前位置: 首页 > news >正文

网站因备案关闭深圳做网站知名排行

网站因备案关闭,深圳做网站知名排行,dw怎么做秋季运动会网站,网址注册在哪里注册[本文大纲] 引言 模板实例化 隐式实例化 显式实例化 模板具体化 显式具体化 部分具体化 函数重载和具体化 类型推断 隐式类型转换 支持的类型转换 引用和const 通用引用、引用折叠和完美转发 … [本文大纲] 引言 模板实例化         隐式实例化         显式实例化 模板具体化         显式具体化         部分具体化         函数重载和具体化 类型推断         隐式类型转换         支持的类型转换         引用和const         通用引用、引用折叠和完美转发         通用引用和右值引用 类型萃取         typename         关联类型         类型萃取         迭代器萃取         特性萃取 - SFINAE特性 auto和decltype         auto         decltype         后置返回值类型         逗号运算符与类型限制         lambda函数 可变参数模板         参数包展开         函数模板使用函数展开         函数模板使用初始化表展开         函数模板使用递归展开         类模板使用继承递归展开 模板函数重载决议         重载匹配规则         指针         列表和数组         类类型         引用和const         通用引用的重载         辅助函数重载匹配 其他问题         模板与继承         模板与友元函数         支持隐式转换         函数回调 引言 C作为常用于高性能开发环境的编程语言不仅提供了对底层的精细控制方法而且为工程开发提供了足够的特性支持C的模板特性就是其中重要的体现之一它的意义体现在以下多个方面 ① 作为泛型编程的基础。C标准库中提供的泛型容器以及其它第三方泛型库普遍依赖C模板。泛型意味着我们在编写代码时可以忽略对象本身具体类型而认为它可以是任意类型在使用的时候才去决定它的类型。 如果我们基于泛型的角度去认识模板就是通过模板可以实现一次编程适配所有类型。如果单是实现这一点那么模板并没有那么复杂引入复杂度的可能来自以下这些内容一是我们总是需要对特定类型做特殊处理二是编译器本身在翻译模板的时候可能会产生的问题比如模板参数会被推断成什么类型、多个特化的情况下如何匹配模板类型等。 ② 实现代码的复用和生成。模板本质上就是编译器在编译期间为我们生成代码我们可以简单地把它理解成一个更高级版的宏定义。 如果基于代码生成的角度去认识模板我们可以基于模板去实现一些高级语言的特性比如反射或者像高级语言一样做一些易用的封装接口比如实现一个通用消息转发类。更进一步我们可以利用代码生成去完成一些编译期的计算。 * 阅读本文需要对左右值的概念有一定的了解需要对模板基础概念有一定的了解 模板实例化 首先需要明确的一点是模板是一个纯编译期的特性它不具备运行时动态的特性并且模板的生成仅在使用模板的时候编译器才会根据模板定义生成对应类型的实例这一特性导致了如下情况的发生 ① 只有实际使用到的模板参数才会被实例化。换言之如果从未调用过某个模板参数那么就不会生成对应的实例。这个规则同样适用于模板类的函数也就是说模板类的函数可以是部分实例化的。 ② 模板不支持运行时动态推断。一个最简单的例子是如果我们定义一个非类型模板参数作为数组大小的模板数组那么它只能是一个静态数组 template typename T, int SIZE class Array { T arr[SIZE]; };Arrayint, 5 arr0; // ok Arrayint, n arr1; // error ③ 模板的声明和定义必须同时写在头文件中不然可能会产生链接错误。这是因为当我们写下模板函数定义的实现的时候我们只是向编译器描述了模板函数应该是什么样的但是编译器并没有真正实例化只有当我们实际调用某个模板函数的时候才会生成对应的实例化结果。假如我们把模板的函数定义放在cpp文件中实例化的时候就找不到函数模板定义。 隐式实例化 当我们在讨论模板的声明和定义能否分离编译的时候我们实际上基于这一事实就是模板是在实际调用的时候才去实例化的这被称为隐式实例化 min(1.0f, 4.0f); // 生成minfloat(float, float)的实例 显式实例化 当然我们也可以直接地指出实例化的类型称为显式实例化。这通常应用于当我们需要频繁使用某个实例化的类型时我们在头文件中完成显式实例化比如 using ObjType ArrayObject, ObjectAllocator; 这样我们就确保了我们可以在不同文件中都去访问ObjType类并且它是一定存在的。 显式实例化只需要像上述这样的声明不需要额外的定义定义是由编译器根据模板内容生成的。 假设我们仅仅需要引用有限数量的模板实例我们也可以利用显式实例化来完成分离编译。但这通常只在少数场景上是有效的。 模板特化/具体化 模板特化/具体化specialization是指我们可能需要为特殊类型的实例化做一些模板的改动。其中包含了显式具体化全特化和部分具体化偏特化两种改动方式。 显式具体化 对于一些特殊类型模板的实现是不合适的所以需要我们对这种情况做特殊处理。这个时候就可以使用显式具体化(explicit specialization)也被翻译为全特化。 比如对const char*的字符串类型我们需要做特殊处理 template bool Compareconst char*(const char* a, const char* b) {//... } 或者对类模板做处理 templatetypename T1, typename T2 class A {T1 a;T2 b; };template class Afloat, int {float a;int b; }; 显式具体化的特点是声明template时尖括号的内容是空的。  部分具体化 我们还可以对部分实例做特殊的模板改动这被称为部分具体化(partial specialization)也被翻译为偏特化。 templatetypename T1 class AT1, int {T1 a;int b; }; 当我们在实现部分具体化时我们在尖括号内仅省略我们具体化的参数而没有具体化的参数仍然保留在尖括号内。 部分具体化只能作用于类模板不能作用于函数模板 函数重载和具体化 具体化实际上只是告诉编译器在生成特定类型的模板函数实例化时需要做什么特殊操作。它有别于函数重载因为它根本就没有生成可供选择的同名函数重载。 类型推断 当我们使用隐式实例化的时候编译器会去推断我们匹配的类型。当我们提供了准确的类型时类型推断一般不会出什么问题但如果我们提供了会产生歧义的类型呢 隐式类型转换 如果我们调用普通的函数并提供了不匹配的类型那么函数可能会通过隐式转换来完成类型转换比如 double min(double a, double b) {return a b ? a : b; }float x min(1.0, 2); 此时2作为int类型可以隐式转换为double类型调用成立。 但是对于模板而言对于不同的类型它更倾向于生成新的类型实例而不是基于已有的类型实例进行隐式转换比如同样对于上述例子如果我们把min函数写成模板函数 templatetypename T T min(T a, T b) {return a b ? a : b; }int x min(1.0, 2.0); // minfloat int y min(1, 2); // minin int z min(1.0, 2); // error 当我们先调用min(1.0, 2.0)时会生成mindouble模板实例随后调用min(1, 2)时会新生成minint的模板实例而不是将int隐式转换为double并调用mindouble的实例。 但如果我们一定要让min(1,2)使用mindouble的实例我们可以使用显式实例化 int y mindouble(1, 2); 如果我们提供了不匹配的输入比如min(1.0, 2)由于模板调用默认不进行隐式转换编译器无法确认生成mindouble还是minint的实例就会发生编译报错。 支持的类型转换 特别的模板仅支持以下有限的类型转换 const/reference 我们同样使用以上模板函数为例假如我们混合输入const和非const的类型 const int a 1; int b 2; int x min(a, b); // minint(); 以上例子是可以通过编译的事实上模板处理值传递时总是会忽略顶层const比如两个参数都是const int类型模板总是会翻译成int。 函数/数组 当我们传入函数或者数组的时候它会退化成函数指针/数组指针相当于发生了类型转换 templatetypename T void func(T param);void test(int param);int arr[2] { 1, 2 }; func(arr); // int*func(test); // void(*)(int) 总体而言模板推断会忽略顶层const/reference、函数和数组。但如果我们强制通过引用来访问则不会发生类型转换 int arr[2] { 1, 2 }; auto arr1 arr; // int()[2]const int ci 1; auto cr ci; // const int 以上设计是可以理解的比如对于数组来说如果对不同大小的数组都生成对应大小的实例化函数那么将会出现代码膨胀。另一方面大小不一致也会不被认为是一个类型。  引用和const 我们在考虑模板的类型推断的时候还需要考虑到变量的修饰属性也就是const和左右值属性。假设我们在声明模板的时候添加了const或是/的修饰符那么将会发生什么呢我们分以下几种情况讨论。 值类型 templatetypename T void func1(T); 正如我们在前面所提及对于非引用的模板参数模板推断会忽略const和引用属性。  int i 1; const int ci 1; int ri i; func1(i); // int func1(ci); // int func1(ri); // int 左值引用 templatetypename T void func2(T); 假如我们在函数参数中添加了的左值限定这个时候用右值作为输入就会出错。 int i 1; const int ci 1; func2(i); // int func2(ci); // const int func2(1); // error 在上述例子中对于包含了const的输入模板会翻译出const。这和模板参数不带任何左右值限定的情况完全不一样。 常量左值引用 templatetypename T void func3(const T); 假如我们在函数参数中添加了的左值限定和const这个时候理论上我们可以输入任何参数并且由于参数本身包含了const我们推断出的结果就不会包含const这是基于const本身可以提升临时变量的生命周期。 int i 1; const int ci 1; func3(i); // int func3(ci); // int func3(1); // int 右值引用 templatetypename T void func4(T); 假设我们在函数参数中添加了的右值我们可以输入右值但同时也能输入左值只是编译器会将左值推断成左值引用类型。 int i 1; const int ci 1; func4(i); // int func4(ci); // const int func4(1); // int 这是一个非常特殊的引用因为它和我们认识的非模板函数的右值引用完全不一样它保留了变量的const/引用属性比如对于数组而言就不会退化成指针 templatetypename T void TestType(T param) {cout typeid(T).name() endl; }Test(abc); // const char[3] 常量右值引用 templatetypename T void func5(const T); 只能接受右值作为输入。 在以上情况中通常我们使用非引用的版本。对于一些常见的作为工具函数使用的模板函数我们其实并不关心其const/引用属性此时它如果生成了const/引用的多个实例化对于开发者来说是一种不必要的代码膨胀。 当我们使用了包含左值/右值的版本时通常我们是为了处理一些和引用/const相关的逻辑比如我们可能想要保留原有参数的const/引用属性或者移除/添加const/引用属性又或者是完成左右值转换等。 比如在标准库中就提供了一些辅助方法 remove_reference // 移除引用属性 add_lvalue_reference // 添加左值引用 add_rvalue_reference // 添加右值引用 注意以上方法作用于类型而不是对象比如 remove_referenceint::type; // - int C标准库中同样也封装了对应的方法比如std::declval可以返回添加右值引用的结果它的内部就引用了add_rvalue_reference template class T typename std::add_rvalue_reference T::type declval ( ) noexcept; 通用引用、引用折叠和完美转发 在我们上面提到的所有引用输入中有一个应用比较广泛的例子那就是右值引用 templatetypename T void func(T); 我们在前面提到了右值引用能够接受所有类型的参数并且当我们输入左值的时候会被推断成左值引用相当于我们得到了这样的结果 void func(int ); 实际上在常规的编程规则中 这样的语法是不合法的它仅仅在右值引用输入的模板中是被允许的语法对应的规则被称为引用折叠 T - T T - T T - T T - T 对于双重引用的情况编译器会将其折叠成一个引用对应的翻译规则如上所示。 也就是说当我们传入左值的时候会被翻译成左值 -)当我们传入右值的时候会被翻译成右值( - )。因此我们将其称之为“右值引用”的模板已经不那么合适了因为它并不如字面意思那样只能接受右值的输入实际上它和我们常说的非模板情况下的右值引用也不是一个东西。通常情况下我们将其称之为“通用引用”universal references)。 另一方面通用引用能够很好地保留参数的左右值/const属性因此我们常常将其用作转发。因为它很好的保留参数的属性我们也将之称为完美转发。 完美转发的意思是函数需要将其输入维持原状传递给另一个函数。 ① 当我们把承担转发作用的函数参数改成通用引用模板()后此时我们就能够正确识别了输入的参数是左值还是右值 ② 即使我们识别了输入的参数的左右值属性由于在函数内部实参本身作为左值存在如果我们在执行转发的时候直接调用实参本身就会丢失左右值属性 ③  为了解决上述问题C标准库引入了forward函数来辅助完美转发的实现相当于我们应该这样编写转发的代码 templatetypename T void func(T param) {otherfunc(std::forwardT(param)); } 也就是我们需要配套使用forward和通用引用来实现完美转发。 标准库中forward的实现如下 // 左值引用 templatetypename T T forward(typename remove_referenceT::type t) noexcept {return static_castT(t); }// 右值引用非通用引用 templatetypename T T forward(typename remove_referenceT::type t) noexcept {return static_castT(t); } 它实现了左值和右值两个版本其中接受左值的版本返回左值接受右值的版本返回右值通过返回值的引用折叠实现。  接下来我们来看一个完美转发的应用例子那就是std::vector中的emplace_back函数。 struct Point {float x 0.0f;float y 0.0f;Point() { }Point(int _x, int _y) : x(_x), y(_y) { } };vectorPoint v; v.push_back(Point(1.0, 2.0)); v.emplace_back(1.0, 2.0); 常规情况下我们会调用std::vector的push_back函数来完成新数据的添加以上例子中我们调用Point的构造函数、拷贝构造函数以及析构函数。即使拷贝构造函数本身可以通过移动语义优化但我们仍然避免不了临时对象的创建。 而使用emplace_back后我们相当于把参数转发到vector内部进行直接构造就不需要借助于临时变量的载体我们去参考C vector的源码就会发现emplace_back内部就是遵循完美转发的语法实现的。 template class... _Valty decltype(auto) emplace_back(_Valty... _Val) {// ..._Ty _Result *_Emplace_reallocate(_Mylast, _STD forward_Valty(_Val)...);// ... } 由此可见完美转发有以下两个重要的作用 ① 保留参数的引用/const属性 ② 避免不必要的拷贝和临时对象 通用引用和右值引用 我们把形如templatetypename func(T)的引用称为通用引用是因为它几乎可以接受所有类型的参数。但这不意味着出现了的形参就一定是通用引用它也可能就是普通的只接受右值的右值引用。 比如对于一个普通函数来说它是一个右值引用 void func(int param); // 右值引用 我们再来看C vector库提供了这样一个方法 void push_back(_Ty _Val) {emplace_back(_STD move(_Val)); } 这里实际上也是push_back一个右值输入的函数重载。虽然这里也出现了模板但和通用引用不一样的是这里的模板是类模板的类型并非函数模板的类型。我们还会发现它搭配了move来使用而不是完美转发配套的forward这也正是因为输入参数是右值引用而非通用引用。 我们再回顾一下forward的实现提供了左值和右值的两个重载版本其中右值引用输入的定义如下 templatetypename T T forward(typename remove_referenceT::type t) noexcept {return static_castT(t); } 这里通过remove_reference巧妙地移除了类型可能存在的引用也就规避了引用折叠因此这个输入只能是一个右值。  类型萃取 对于模板而言我们通常会需要获得模板的类型信息这种在编译期间获取类型信息的技术被称为类型萃取type traits。 typename 在讨论类型萃取之前我们先来简单介绍模板定义中一个非常常见的关键词typename。实际上我们在前面已经多次见到这个关键词了我们至少见到了它的以下两种用法 ① 作为模板类型 templatetypename T void func() { } templateclass T void func() { } 在这个例子中typename和class的使用并无差别。 ② 描述对象是一个类型 我们在前面介绍了可以修改类型引用/const属性的一系列方法比如remove_referenceT::type它会返回一个类型。 但是这个表达是存在歧义的我们也可以认为这是在访问类的静态成员变量。为了让编译器识别到这是一个类型而不是一个变量我们需要用typename来修饰。这也就是为什么我们在forward函数实现时使用typename remove_referenceT::type来表达类型。 我们通常也会使用using或typedef简化如上书写 // (1) template class _Ty using remove_reference_t typename remove_reference_Ty::type;// (2) typedef typename Iterator::value_type value_type; // 新版本使用using 两者差别不是很大using相比起来更符合C编程风格stl中using的使用更为广泛。 当我们将别名应用到模板参数中时我们像这样使用 templatetypename XXX 关联类型 对于模板而言我们已知templatetypename T那么直接使用T就能获取对应的类型那么类型萃取听起来并不是用于获取本身的类型的它的应用场景更多的出现在获取与当前类型”相关联“的类型。 在前文中我们介绍了可以修改左右值属性的方法并且我们提到了这些方法是作用于类型的。比如我们多次提到的remove_reference就是类型萃取的一个例子就像我们在forward函数的定义中一样我们通过调用remove_referenceT::type获得类型T移除引用后的类型。 通过上述例子我们了解到了类型萃取的一个应用那么就是获取当前类型的关联类型也就是我们已知类型T通过remove_referenceT::type得到了T移除引用后的类型。 类型萃取 在这里我们是作为类型萃取的使用者通过库开发者提供的::type方法获取类型。同样的我们也可以按照相同的形式在接口开发的时候提供给使用者必要的类型信息比如提供这个类型的一个关联类型信息。 我们假设我们有不同的类型的工厂每个类型的工厂都会生产一种特定的产品那么我们就认为特定类型的工厂和对应类型的产品这两个类是相关的。 其中一个工厂是苹果工厂它可以生产苹果我们先定义这两个类 class Apple { // ... };class AppleFactory { // ... }; 然后我们用萃取类来定义这两个类的关联也就是输入工厂的类型就能得到工厂生产对象的类型萃取类被设计为一个模板类 templatetypename T class FactoryTraits { public:typedef typename T::ValueType ValueType; }; 接下来我们通过类的特化来定义具体的关联 template class FactoryTraitsAppleFactory { public:typedef Apple ValueType; }; 此时我们就完成了萃取的所有定义。我们注意到该例子中AppleFactory和Apple本身并不是模板类只有萃取类本身是模板类。如果我们希望使用我们上述定义的萃取我们需要在模板中使用它们 templatetypename T class FactoryContainter { public:typedef typename FactoryTraitsT::ValueType ObjType;ObjType Produce(){// ...} }; 如上所示借助萃取类当我们将FactoryContainer实例化为特定的Factory时我们也能在FactoryContainer中访问到它的关联类。 FactoryContainterAppleFactory::ObjType a; // Type Apple 当我们输入不同类型的Factory都能通过::ObjType的统一写法获取对应的对象类型。  萃取类本身的实现并不难只是提供一些预设的类型它更像是一种编程约定与规范也就是描述了可以提供哪些具体的类型并且这些类型是需要在显式实例化中提供的。或者说正是因为有了萃取类的存在我们才有办法获取关联类的信息。 迭代器萃取 我们再来看一个C标准库的例子容器元素和迭代器这两者也是具有一定关联的类。 当我们定义迭代器的时候可以通过迭代器萃取类来获得容器元素的类型 template class Iterator struct iterator_traits {typedef typename Iterator::value_type value_type; }; 我们通过如下调用访问类型 typename iterator_traitsT::value_type; 这里的value_type是指值类型通常我们需要提供所有限定符的类型 template class Iterator struct iterator_traits {typedef typename Iterator::iterator_category iterator_category;typedef typename Iterator::value_type value_type;typedef typename Iterator::pointer pointer;typedef typename Iterator::reference reference;typedef typename Iterator::difference_type difference_type; }; 这意味着假如我们要实现迭代器Iterator我们应该按照上述形式提供萃取类的实例化即必须提供以下五种类型iterator_catergory、value_type、pointer、reference、difference_type只有当我们提供了所有类型才能通过迭代器访问类型。 其中iterator_category标识当前容器支持哪种级别的迭代器访问输入输出迭代器、前向迭代器、双向迭代器、随机访问迭代器。 特别的标准库提供了指针类型的特化版本用来处理指针类型无法正常访问到value_type的情况。 // partial specialization templateclass T struct iterator_traitsT* {typedef T value_type; }; templateclass T struct iterator_traitsconst T* {typedef T value_type; }; 特性萃取 - SFINAE特性 类型萃取可以让我们获得这个类型“是什么”那么进一步的当我们有办法知道类型是什么的时候我们就可以基于类型萃取获取这个类型“是怎样的”信息。 获取类型特性 templatetypename T struct is_void {static const bool value false; };template struct is_voidvoid {static const bool value true; };is_voidint i1; // value false is_voidvoid i2; // value true 以上是一个非常简单的例子我们先定义一个模板函数is_void默认值为false 然后我们提供一个显式具体化版本is_voidvoid默认值为true。 那么通过调用is_void我们就能获取这个类型是不是void也就是说当我们匹配void失败后就不去生成特化的版本。 基于类似这样的写法我们就可以实现类型特性的判定比如在C标准库中就提供了大量相关的预设方法 is_integral is_enum is_floating_point is_arithmetic is_class is_rvalue_reference is_lvalue_reference is_scalar is_compound 对于上述方法通常来说如果我们访问::value就能得到它返回的值。  我们再来看一个利用模板函数参数匹配来实现的is_class方法它比is_void的实现要复杂一些可以用来判断输入的对象是不是一个类原理是只有是类类型时才能够调用对应函数根据函数的不同返回值来确认最终调用了哪个重载 templatetypename T class is_class {typedef char yes[1];typedef char no[2];templatetypename Cstatic yes test(int C::*); // 只接受类的输入templatetypename Cstatic no test(...); // 通配符 public:static bool const value sizeof(testT(nullptr)) sizeof(yes); }; 当我们具备了识别类型特性的能力后我们就可以用它来做一些静态类型的检测比如之前介绍的forward的右值引用版本实际上就添加了静态检测 templatetypename T T forward(typename remove_referenceT::type t) noexcept {static_assert(!is_lvalue_reference_v_Ty, bad forward call);return static_castT(t); } 广义的模板具体化  如果仅仅适用于静态类型检查那么特性萃取看起来作用面并没有那么广。但我们可以把静态类型检查的特性推广到模板的具体化上。 我们通常把实现这一点的这种特性称为SFINAE特性也就是Substitution failure is not an error。它字面上的含义是类型匹配失败时放弃该特化而不是由于失败而产生编译错误。 templateclass Tvoid f(typename T::Y*) { } 比如上述例子中如果类型T并不包含类型Y那么就认为替换失败将不会选择使用这个重载。 我们在前面提到我们可以通过明确指定Type是什么来实现模板的显式具体化我们也可以通过对一类特定类型比如指针实现部分具体化。但我们在前面并没有解释如果我们想要对另外一些“满足某些条件”的类型执行特定的具体化应该如何实现比如也许我们希望对enum类型做一些特殊处理我们期望的代码如下 tempaltetypename T class CT, (if T is enum) { }; 为了达成这个目的我们可以利用SFINAE特性也就是我们在上述代码中if T is enum)的占位符中提供一个仅在T为enum的时候有意义的语句。如果T不是enum那么就让它替换失败。 我们已经有了is_enumT::value可以返回布尔值判断类型是否是enum。但是只通过一个布尔值我们无法得到无意义的语句因此标准库中还提供了enable_if来辅助实现这一点 templatebool predicate, typename result void class enable_if;tempaltetypename result class enable_iftrue, result { public:typedef result type; };templatetypename result class enable_iffalse, result { }; 在这个模板类中如果表达式为true我们就能够通过enable_ifexpression::type获得类型否则enable_ifexpression:::type将不会被定义。 我们在模板中包含enable_if并且传入is_enumT::value作为第一个参数如果is_enumT::value为false那么enable_ifexpression:::type就不存在编译器会忽略该替换。 我们最终的实现是 templatetypename T, typename Enable void class A { };templatetypename T class AT, typename enable_ifis_enumT::value::type { };enum EName { }; int main() {Afloat a1;// primary templateAEName a2; // partial template } 但是如上的书写有些过于麻烦了我们可以用预设的enable_if_t来简化书写  templatetypename T class AT, enable_if_tis_enumT::value { }; 在上述例子中我们演示了enable_if的一种用法那就是作为作为模板的偏特化并且是一个额外类型。该额外的类型可以不参与到真正的类型运算中仅仅便于编译器推断条件是否成立由于提供了默认值void使用者也不需要指定这个参数就像它不存在一样。 总体而言enable_if可以出现在很多地方包括类的模板参数、偏特化参数、函数模板参数、函数参数等等如下所示 模板类的偏特化参数 偏特化的使用我们在前面介绍过我们还可以提供多种偏特化版本 templatetypename T class AT, enable_if_tis_enumT::value { public:A() { cout A enum endl; } };templatetypename T class AT, enable_if_tis_integralT::value { public:A() { cout A int endl; } }; 模板类的默认模板参数 我们可以将enable_if作为模板类的模板参数 templatetypename T, typename enable_if_tis_enumT::value class A { public:A() { cout A enum endl; } }; 如果定义了两个模板类而默认模板参数不一样编译器会认为这是重复的类模板定义也就是说如下代码将无法通过编译  // error ! templatetypename T, typename enable_if_tis_enumT::value class A { public:A() { cout A enum endl; } };templatetypename T, typename enable_if_tis_integralT::value class A { public:A() { cout A int endl; } }; 模板函数的默认模板参数 我们可以将enable_if作为模板函数的模板参数。 templatetypename T, typename enable_if_tis_integralT::value void func() { cout int endl;}; 如果定义了两个函数模板而默认模板参数不一样编译器会认为这是重复的函数定义而不会将其视为函数重载也就是说如下代码将无法通过编译 // error ! templatetypename T, typename enable_if_tis_integralT::value void func() { cout int endl;};templatetypename T, typename enable_if_tis_enumT::value void func() { cout enum endl;}; 模板函数的非类型模板参数 我们还可以将enable_if的值作为非类型模版参数作用于类模板也就是将enable_if的第二个模板参数设置为bool这样就能返回true/false的结果 templatetypename T, enable_if_tis_integralT::value, bool true void func() { cout int endl;}templatetypename T, enable_if_tis_enumT::value, bool true void func() { cout enum endl;}通过这样的方式就能够实现不同类型特例化的“模板函数重载”而不会发生编译报错。 模板函数的默认函数参数  前面的例子中enable_if都是应用在模板参数列表之中。实际上enable_if也可以应用于函数参数之中作为一个无需指定的默认参数如下所示 templatetypename T void func(typename enable_ifis_enumT::value::type* 0) {cout enum endl; }templatetypename T void func(typename enable_ifis_integralT::value::type* 0) {cout int endl; } 模板函数的返回值 enable_if可以用作函数参数同样也可以作用于函数返回值其中std::enable_ifstd::is_enumT::value::type的结果是void。 // return void templatetypename T typename enable_ifis_enumT::value::type func() {cout enum endl; }templatetypename T typename enable_ifis_integralT::value::type func() {cout int endl; }// return T templatetypename T typename enable_ifis_enumT::value::type func1() {cout enum endl; } 使用生成的类型 我们在前面介绍的例子都是利用了enable_if的特性来控制模板的生成但是我们并没有实际使用它本身的类型第二个参数而是默认将其视为void。如果我们将其设置为T则能够将其视为类型T使用。 定义输入为T templatetypename T void func(typename enable_ifis_integralT::value, T::type t) {cout int endl; } 定义返回值为T templatetypename T typename enable_ifis_integralT::value, T::type func() {cout int endl;return 1; } 控制输入类型 在上一节中我们介绍了enable_if的可以应用的场景模板参数模板非类型参数函数参数函数返回值等并且介绍了enable_if的一个使用实例就是实现广义的模板显示具体化。它成立的逻辑是如果类型满足enable_if的条件那么生成对应的实例化否则使用通用模板的实例化。此时如果我们并没有提供可用的“通用版本”那么当类型不满足enable_if的条件时将无法找到合适的重载此时就会发生编译报错。 利用这个特性我们可以实现对类模板/函数模板的类型校验和限制。也就是说只有满足了条件的才会生成对应类模板或函数模板否则就会发生编译报错或者编译器会选择其它重载。 比如在前面的函数模板的例子中如果我们只提供了int的特化版本 templatetypename T, enable_if_tis_integralT::value, bool true void func() { cout int endl;} 此时我们调用funcfloat()就会发生编译报错。  控制返回类型 利用enable_if我们可以实现不同条件下返回值类型的控制。比如我们希望在i为0和不为0时获得不同类型的返回结果 templateint i typename enable_ifi 0, int::type func() {return 0; }templateint i typename enable_ifi ! 0, float::type func() {return 0; }auto t1 func0(); auto t2 func2(); cout typeid(t1).name() typeid(t2).name() endl; // int, float auto和decltype 我们都知道C中有两个和类型自动推导相关的关键字auto和decltype。这两者一方面让我们写代码变得更方便比如我们常常会用auto来代替那些名字很长的类型定义的书写另一方面它们在适配模板自动类型推导也非常有用。 在此之前我们先简单的介绍一下auto和decltype的语法。总体而言两者都能推导出表达式/变量的类型只不过auto更倾向于移除表达式/变量的reference/const属性decltype更倾向于保留表达式/变量的reference/const属性。 auto 编译器推断auto的类型和原始类型有时不一定相同如前所述它可能会有如下差异 1.忽略引用 对于引用类型翻译为引用对象本身的类型。 int i 0; int ri ; auto a r; // int 2.忽略顶层const保留底层const const int ci 1; const int cr ci; auto b ci; // int auto c cr; // int auto d ci; // const int* 3.auto配合和const 我们可以手动添加推导对象的/const属性 const auto e ci; // const int auto f ci; // const int auto的类型推导基本和模板推断类型的逻辑一致我们在前面介绍的模板类型匹配规则也同样适用于auto。 decltype decltype基本保留变量和表达式原本的类型 1.变量 处理变量时会保留变量的顶层const和引用 const int ci 0; const int ri ci; decltype(ci) a 0; // const int decltype(ri) b a; // const int 对变量添加括号会被认为是表达式。变量是出现在赋值语句的表达式它将返回引用类型 const int ci 0; decltype((ci)) c ci; /todo 2.表达式 编译器在会分析表达式返回类型但不会实际计算表达式本身表达式按照实际情况返回结果 int a 0, b 1; int* p a; decltype(a b) x; // int decltype(*p) y b; // int auto/decltype都是编译时确定类型的因此它非常适合应用在同样在编译期确定代码的模板辅助我们获取编译期已知但编写代码时还未知的类型。  后置返回值类型 auto/decltype的一个比较常见的用法是用于辅助推导返回值类型。 假如我们想要用decltype推导加法的返回类型我们可以这么写 decltype(x y) z x y; 把这个方法扩展成模板函数我们无法像如下这样常规的形式编写因为解析到decltype(t1 t2)时t1和t2变量还没有定义。 // error templatetypename T1, typename T2 decltype(t1 t2) add(const T1 t1, const T2 t2) {return t1 t2; } 为了解决这个问题C支持了类似lambda函数的后置返回值表达方式 templatetypename T1, typename T2 auto add(const T1 t1, const T2 t2) -decltype(t1 t2) {return t1 t2; } C14中 提供了更为简洁的decltype(auto)语法来代替复杂的后置的写法 templatetypename T1, typename T2 decltype(auto) add(const T1 t1, const T2 t2) {return t1 t2; } 此外我们还可以直接使用auto作为返回值它和decltype(auto)的区别和auto与decltype一样可能不会保留变量的const/reference属性 templatetypename T1, typename T2 auto add(const T1 t1, const T2 t2) {return t1 t2; } 在有些情况下保留const/reference属性非常重要比如下述使用下标访问对象时我们期望返回的是一个左值这样才满足我们可以直接对下标对象赋值的需求 templatetypename T decltype(auto) At(T container, int index) {return container[index]; }vectorint vec{1,2,3,4}; At(vec, 0) 0; // int cout vec[0] endl; // output : 0 逗号运算符与类型限制 我们知道逗号运算符中会去计算第一个操作数和第二个操作数的结果并且丢弃第一个操作数的结果并保留/返回第二个操作数的结果。 在后置返回值类型中如果我们将逗号运算符应用到decltype相当于向decltype输入一个表达式表达式会返回第二个操作数的结果因此我们把实际返回的类型放在第二个操作数的位置在第一个操作数可以放置用于类型限制的操作数。 比如下述代码限制类型T为Class类型F为C的成员函数的指针并且返回void ​// from cppreference.com templateclass C, class F auto test(C c, F f) - decltype((void)(c.*f)(), void()) { } ​ lambda函数 在C14中支持了在lambda形参中使用auto关键字它实际上相当于在lambda函数中实现了函数模板 auto f [](auto param) { ... }; 不过templatetypename T是隐藏的这也就意味着我们无法通过T来直接访问参数类型这时我们必须依赖于auto/decltype的类型推导 auto f [](auto param) {auto param1 param; };auto f1 [](auto param) {remove_referencedecltype(param)::type param1; }; 如果我们要在lambda函数中实现参数的完美转发可以按如下方式来实现 auto f [](auto param) {func(std::forwarddecltype(param)(param)); };可变参数模板 到目前为止我们已经介绍了模板的不少高级特性我们已经可以完成对任意类型的匹配并且能够对类型实现控制。但是对于泛型而言这一切还没那么完善因为我们的参数数量还是固定的。这意味着如果我们想要实现一个通用转发调用我们需要按如下方式书写 templatetypename T1 void func_OneParam(T1 t1) { };templatetypename T1, typename T2 void func_TwoParam(T1 t1, T2 t2) { };templatetypename T1, typename T2, typename T3 void func_ThreeParam(T1 t1, T2 t2, T3 t3) { };// ... 这会使得编码过程变得比较繁琐而且扩展性较差因为不知道需要预留几个参数的版本。我们期望仅编写一次就能处理所有情况换句话说我们需要一个能够输入任意数量和类型参数的方法。 实际上在C语言中我们已经见过类似的方法那就是函数的通配符用...来表示最经典的例子就是可以打印任意类型的printf函数 int printf(const char* format, ...) 我们在前面介绍的is_class实现中为了避免输入类型不是“类”类型时找不到合适重载函数就是使用的通配符...的函数作为fallback。 同样的C模板也提供了类似于通配符的可变参数它可以支持任意类型和数量的输入参数可以应用到模板类和函数模板中它的语法如下 templatetypename... Args class C; // 可变类模板参数 templatetypename... Bases class D : public Bases... { } // 可变参数作为基类 templatetypename... Args void func(Args... args) {} // 可变函数参数其中typename... Args说明Args是一个模板参数包parameter pack可以接受0个或多个模板参数。 Args... args说明args是一个函数参数包可以接受0个或多个函数参数 对于函数参数包使用sizeof...args可以获取当前参数包里有多少个参数。 为了读取参数包里的参数我们需要对参数包进行“解包”这个过程被称为参数包展开parameter pack expansion。 参数包展开 如果我们将省略号放在包含了参数包的pattern的右侧那么我们可以称其为参数包展开 Pattern... 比如args...就是一个最简单的参数包展开它可以被展开为arg0, arg1, arg2, arg3... pattern只需要包含至少一个参数包它还可以有很多形式如下 args... // 展开为arg0, arg1, arg2 std::forwardArgs(args)... // 展开为std::forwardArgs(args0) std::forwardArgs(args1)... args; // 展开为arg0, arg1, arg2... (args, 0)... // 展开为(arg0,0), (arg1, 0)... 展开之后会得到参数包里每个参数的多个pattern。  参数包展开只能发生在有限的位置比较常见的是 ① 函数参数列表 func(args...) auto func [args...]() { } ② 初始项/初始列表 Class c(args...) { args... }③ 模板参数列表 vectorT, Args... 如果我们想要从参数包中解析出每个独立的参数我们需要一个具体的实现函数这个函数也是一个常规的模板函数我们对每个参数的具体处理都写在这个函数里 templatetypename T void funcImpl(T value) {// ... } 我们现在期望的是展开为如下形式funcImpl(args0)funcImpl(args1)funcImpl(args2)...       这样我们就能保证对每个参数都执行了一遍对应的逻辑因此我们的参数包展开如下 funcImpl(args)... 这里我们的pattern是funcImpl(args)结果是void这并不合规为了让这个参数包展开合规我们可以给funcImpl随便添加一个返回值 templatetypename T int funcImpl(T value) {// ...return 0; } 或者我们也可以不给funcImpl加返回值而是使用括号表达式避免结果是void因为括号表达式返回的结果是第二个值 (funcImpl(args), 0)... 函数模板使用函数展开 我们已经得到了一个参数包展开的样例但我们刚刚提到参数包展开只能出现在特定的位置因此虽然以上参数包就已经能够实现所有的逻辑但在语法上不合规我们还需要套一个壳子来使它语法正确虽然这个”壳子“可能没有实际上的作用。 假如我们使用函数来展开 templatetypename... Args void helper(Args...) {}templatetypename... Args void func(Args... args) {helper(funcImpl(args)...); // 或者helper((funcImpl(args),0)...); } 它得到的展开结果可能是 helper(funcImpl(arg2), funcImpl(arg1),funcImpl(arg0)); 这是因为函数参数的求值顺序是不固定的因此在我们对顺序有要求的情况下函数不是一个好的选择。 函数模板使用初始化表展开 如前所述参数包展开还可以出现在花括号里因此我们可以构造一个同样没有实际作用的初始化对象作为壳子来实现展开 templatetypename... Args void func(Args... args) {int arr[] { (funcImpl(args),1)...}; } 由于初始化表的求值顺序是确定的因此我们可以得到如下展开结果 int arr[] { (funcImpl(arg0),1), (funcImpl(arg1),1), (funcImpl(arg2),1)}; 函数模板使用递归展开 还有一个比较常见的方法是使用递归函数展开但由于它用到了递归性能肯定没有直接展开那么好但是代码阅读起来更加直观易懂。 常见的例子就是打印的递归展开 递归展开中通常包含两个函数递归函数和终结函数这两者缺一不可。但参数包只剩最后一个参数的时候就会调用到终结函数。  在C17中我们可以使用折叠表达式来简化递归展开的写法避免一定要使用两个函数实现递归展开。 我们在前面介绍了模板函数参数包展开的做法除此之外我们还可能会使用到模板类比如C中可以支持任意数量类型的tuple类 tupleint, char, string t(1,a,test); 为了对模板类进行展开我们使用了继承递归的方式 类模板使用继承递归展开 使用继承实现类模板的展开时同样需要包含递归终止类和递归展开实现类同时需要类模板的前向声明 // 前向声明 template class... _Types class tuple;// 递归终止类 template class tuple { // empty tuple public:// ... };// 继承递归实现类 template class _This, class... _Rest class tuple_This, _Rest... : private tuple_Rest... { // recursive tuple definition public:// ...constexpr _Mybase _Get_rest() noexcept { // get reference to rest of elementsreturn *this;}_Tuple_val_This _Myfirst; // the stored element }; 通过_Myfirst访问当前元素通过_Get_rest()访问剩下的元素*this指向父类 模板函数重载决议 在前文中我们了解到我们可以定义多个同名函数包括常规函数模板函数以及模板函数的显式具体化版本。如果我们同时定义了多个版本的同名模板函数并且输入参数也一致那么编译器又会如何选择对应的匹配函数呢 总体而言编译器会去优先选择更加“特化”的版本它遵循如下优先级普通函数 - 显式具体化函数全特化 -部分具体化函数偏特化- 通用模板函数。只有这样我们额外定义的具体化函数才可能会生效。 但如果参数不那么一致又同时有普通函数和模板函数我们又该如何决策呢 重载匹配规则 我们在实现模板函数的重载时遵循如下的步骤 1.编译器先替换显式具体化特化的模板参数 2.编译器确定模板参数的类型 3.编译器将函数参数/返回值、模板参数值/表达式替换成该类型 4.如果替换失败就从重载集中丢弃它 5.如果替换成功对比替换之后的函数和其它函数更匹配的重载 6.如果替换成功的函数参数完全一样并且更加特化的版本遵循普通函数-显式具体化函数-通用模板函数的规则选择更合适的重载 7.如果替换成功的函数同名但参数不一致按照更匹配的参数选择重载 8.如果无法决议出更匹配的参数说明发生了重载歧义编译报错 ● 简单来说编译器会先完成模板参数替换然后再去做重载决议决议时如果参数一致选更特化的版本如果参数不一致选更匹配的参数 ● 重载集中包含了普通函数和替换后的模板函数它们均可作为重载函数的候选 替换的位置可能发生在 1.函数定义中的类型/表达式包括返回值/输入参数 2.模板参数定义中的类型/表达式 3.模板显式具体化的参数中的参数/表达式 正是因为替换的位置非常多我们在应用SFINAE特性的时候才可以将类型判断逻辑如enable_if放在多个地方因为这些地方都会参与编译期的替换计算。但需要注意的是函数体内部并不会参与替换的过程。 以下是一些具体的模板函数重载示例 指针 模板函数重载的一个比较常见的做法是在提供常规模板的同时提供指针和常量指针的特化版本这是因为通常而言指针类型会需要特殊处理。 当我们提供了指针类型的特化时指针类型就会优先匹配对应的模板函数 templatetypename T void Test(T t) {cout Test(T t) endl; }templatetypename T void Test(T* t) {cout Test(T* t) endl; }templatetypename T void Test(const T* t) {cout Test(const T* t) endl; }const int i 2; const int* cp i; Test(cp); // Testint(const int*) 列表和数组 常规模板无法推断出花括号初始化列表的类型因此如果我们想要接收对应类型需要手动提供initializer_list的重载如下所示 templatetypename T void func(T param);templatetypename T void func(std::initializer_listT initList);func({ 1, 2, 3}); // match void func(std::initializer_listint) 同理我们可以为数组类型提供重载 templatetypename T void func(ArrayT arr) { } 类类型 如果我们想要确保输入是一个“类”类型我们可以提供如下版本的重载 templatetypename T void func(T param);templatetypename T void func(T::* param);引用和const 我们在前面介绍了带有引用和const的同名函数的匹配规则。如果我们已经定义了引用版本的重载函数再去定义常规的模板函数那么重载决议就会发生冲突因为编译器无法决定哪个匹配更优 templatetypename T void Test(T t) {cout Test(T t) endl; }templatetypename T void Test(const T t) {cout Test(const T t) endl; }templatetypename T void Test(T t) {cout Test(T t) endl; }int i 1; const int ci 2; Test(i); // error! match Test(T t) Test(T t) Test(2); // error! match Test(T t) Test(T t) Test(ci); // error! match Test(T t) Test(const T t) 因此我们在提供了引用版本的输入后一般不再提供常规版本。 对于剩下的模板重载函数根据我们前文介绍的规则相同的输入也有可能同时正确匹配到多个函数上但是对于编译器而言可以决议出更优的结果因此不存在歧义。 对于左值非const变量优先匹配左值引用再匹配通用引用最后才去匹配常量左值引用 Test(i); // Test(T) - Test(T) - Test(const T) // i并非常量所以不会优先匹配const版本 // i是左值所以左值引用比通用引用更匹配 对于右值优先匹配通用引用 再匹配常量左值引用它无法匹配左值引用 Test(1); // Test(T) Test(const T) 对于左值const变量优先匹配常量左值引用再匹配左值引用最后才去匹配通用引用 Test(ci); // Test(const T) - Test(T) - Test(T) 通用引用的重载 我们已经介绍了引用/const的函数重载决议规则虽然我们是基于模板函数来介绍的但是这套规则同样适用于普通函数包括模板函数和普通函数共存的时候。正如我们在介绍函数的重载决议规则中所提我们会完成模板函数的参数替换再用替换后的函数”公平的“和普通函数做对比。 我们在前面提到我们会使用通用引用来实现完美转发如下 class Test { public:templatetypename TTest (T t) { ... } }; 对于以上类根据规则编译器会默认生成拷贝构造函数它和完美转发的构造函数正好构成了重载关系。 此时如果我们试图去调用拷贝构造函数根据我们介绍的函数重载决议规则对于非常量左值而言T是比const T更优的匹配因此它实际上并不会调用拷贝构造函数 Test t1; Test t2(t1); // call Test(T) 辅助函数重载匹配 由此可见通用引用的“匹配”能力很强这会使得函数重载决议的结果往往和我们的预期不一致。 有时我们会选择绕开函数重载比如提供一个名字不一样的函数但有时我们绕不开函数重载比如上例中的构造函数。此时我们就需要通过SFINAE特性来辅助函数重载也就是说我们限定只有特定类型能匹配到通用引用。换句话说就是我们期望匹配到拷贝构造函数的时候不应该匹配到通用引用。 那么什么时候应该匹配到拷贝构造函数呢也就是输入类型和类类型完全一致的时候因此我们可以这样实现 class Test { public:templatetypename T, typename enable_if_t!std::is_sameTest, typename std::decayT::type::valueTest(T t); }; // decay : remove const/reference 我们再来看C标准库中的一处使用 vector(const size_type _Count, const _Ty _Val, const _Alloc _Al _Alloc()) { }template class _Iter, enable_if_t_Is_iterator_v_Iter, int 0 vector(_Iter _First, _Iter _Last, const _Alloc _Al _Alloc()) { } 这里给出了vector的两种构造方式一种是指定元素数量和默认值另一种使用迭代器的首尾进行构造这里如果我们没有对第二种构造函数做迭代器类型的限制当我们期望调用第一个构造函数且在元素类型和size_type一致的时候就很有可能错误地匹配到第二种构造函数。 其他问题 模板与继承 如果我们想要在派生类中访问基类的内容如果这是一个模板类那么和普通类不一样基类的名称对派生类不是直接可见的所以我们无法通过常规的访问方式。 ① 使用this访问基类 我们在前面介绍tuple时介绍了tuple访问其余元素的一个方法_Get_rest()我们可能会注意到它访问了*this。仔细一想tuple是一个递归定义类每个tuple只包含了当前的第一个元素为什么*this就能表示剩余元素的语义呢 template class _This, class... _Rest class tuple_This, _Rest... : private tuple_Rest... { // recursive tuple definition public:constexpr _Mybase _Get_rest() noexcept { // get reference to rest of elementsreturn *this;} };实际上在当前语境下这里的this是指基类对象。如果tuple包含当前第一个元素那么基类就包含剩下的元素。 同理如果我们想要访问模板基类的函数也可以通过this-来访问 templatetypename T class Base { public:void BaseCall() { } };templatetypename T class Derived : public BaseT { public:void Call(){this-BaseCall(); // okBaseCall(); // error} }; 我们不能直接调用基类函数是因为模板T没有确定时父类还没有实例化编译器无法确认基类是否包含对应函数在搜索函数定义的时候也不会把基类加入到查找空间。使用this-相当于告诉编译器该函数的定义来自基类。 2. 使用using访问基类 我们还可以使用using语句这是因为using本身就可以用来在派生类中访问隐藏的基类名称 templatetypename T class Derived : public BaseT { public:using BaseT::BaseCall;void Call(){BaseCall(); // ok} }; 模板与友元函数 对于模板类而言相比起常规类它的友元函数的情况会更加复杂比如我们可能要去考虑这个友元函数是共享的还是每个实例独立的友元类型和模板类型是否有关联。 根据以上这些情况我们可以把友元函数大致划分成三种情况 ① 非模板友元 如果友元函数不是模板函数那么仅存在一个友元函数且它是所有实例的友元 void friendfunc() {cout call friend func endl; }templateclass T class C { public:friend void friendfunc(); }; ② 约束的模板友元 如果友元函数是模板函数并且它的类型和模板类的类型有关联那么每个类的具体化有一个对应的友元函数 templatetypename T void friendfunc() {cout call friend func endl; }templatetypename T class C { public:friend void friendfuncT(); }; ③ 非约束的模板友元 如果友元函数是模板函数并且它的类型和模板类的类型没有关联那么每个友元函数的具体化可以对应每个具体化的类 templatetypename T void friendfunc() {cout call friend func endl; }templatetypename T class C { public:templatetypename Ufriend void friendfuncU(); }; 我们已经介绍了模板的一些常用高级特性接下来我们关注一些更加偏向应用的内容。 支持隐式转换 我们知道模板类型倾向于生成一个“新的类型”而不是进行隐式转换实际上模板推断也几乎不支持隐式转换在这个情况下假如我们想要实现隐式转换可以借助成员函数模板来实现比如智能指针模拟指针的隐式转换 templatetypename T class SmartPtr { public:templatetypename USmartPtr(const SmartPtrU other): ptr(other.get()){}; }; 我们将这种写法称为通用构造函数通过这种写法我们可以实现比如派生类向基类的转化 SmartPtrBase p SmartPtrDerived(new Derived); 函数回调 如果我们想要实现一个消息处理机制类能够执行函数回调我们也可以利用模板来实现。 为了实现这一点我们需要使用嵌套的模板类。首先我们需要实现一个模板函数类能够生成任何类型的函数其次我们实现一个面向用户的函数类提供更像函数的封装。 对于定义的函数类而言 首先我们需要缓存一个函数指针我们可以将函数设计为模板。 此外我们还需要调用这个函数因此还需要知道参数类型和返回类型。 考虑到函数调用包含一个返回值和任意数量的参数输入我们设计两个模板参数一个普通模板参数一个可变模板参数来替代单一模板参数考虑到参数的转发我们需要使用通用引用。 为了提供像普通函数一样的调用方式我们需要实现operator()的重载 template typename Ret, typename... Params class FuncImpl { public:using FuncType Ret(*)(Params...);FuncImpl(FuncType func) {funcImpl func;}Ret operator()(Params... params) {return funcImpl(forwardParams(params)...);} private:FuncType funcImpl; }; 以上就实现了基本的函数封装只是对于调用者而言不够友好。我们再设计一个面向用户的函数类并将FuncImpl作为它的一个成员实现具体的函数存储和调用。 我们可以对类型格式进行限制class FuncRet(Params...)这样我们就可以通过类似Funcvoid(int)的形式来调用。 template typename Ret class Func;template typename Ret, typename... Params class FuncRet(Params...) {using CallableType FuncImplRet, Params...;CallableType* Callable nullptr; public:Func(typename CallableType::FuncType func): Callable(new CallableType(func)) { }Ret operator()(Params... params){return (*Callable)(forwardParams(params)...);} }; 以上只是一个初步可执行的简化类更详细的内容我们可以参考std::function的实现与这里不一样的是stl中使用了继承而不是组合的方式来包含可调用对象。
http://www.pierceye.com/news/384480/

相关文章:

  • 免费做一建或二建题目的网站colorway wordpress
  • 简单网站建设合同贵州省高层建筑信息平台
  • 手机网站登录模板电视剧百度风云榜
  • 一嗨租车网站建设的功能特色梅林做网站
  • 网站关于我们怎么做36氪 wordpress 模板
  • 医疗网站建设计划书菏泽手机网站建设
  • 南京外贸网站建设哪家好免费网站建站方法
  • 文化馆建设网站网架公司有哪些
  • 企业如何申请网站51网站空间相册
  • 自己电脑做网站服务器系统网站建设违约交付
  • 什么叫域名访问网站wordpress app 接口
  • 学生网站建设实训总结工信部备案号查询平台
  • 凡科建站如何制作论坛备案网站需要多久
  • 网站建设的公司哪家是上市公司专业外贸网站制作
  • 建站公司杭州免费投票网站制作
  • 网站优化公司效果网络营销毕业后做什么工作
  • 移动互联网的应用论文可以优化网络的软件
  • 网站建设软件哪个最好郑州广告设计与制作公司
  • 浦口区网站建设售后保障如何维护网站
  • 企业网站建设 安全合肥做网站加盟
  • 水果网络营销策划方案电商网站怎样优化
  • 免费数据源网站wordpress主页面
  • 做网站百度收费吗青岛冠通市政建设有限公司网站
  • 菜鸟建网站福建福州罗源建设局网站
  • 企业内网网站制作自己的网站多少钱
  • 关于公司网站建设的申请wordpress站群功能
  • 外贸做企业什么网站珠海的网站建设
  • 做网站教程百度云外贸soho建站公司
  • 上海市网站建设网站增加导航栏
  • 电子政务网站模版网站制作排名优化