C++ template

您所在的位置:网站首页 函数模板与模板函数的关系 C++ template

C++ template

2024-07-17 19:23| 来源: 网络整理| 查看: 265

本篇是本系列博文最后一篇,主要讲解函数对象和回调的相关内容。函数对象(也称为仿函数)是指:可以使用函数调用语法进行调用的任何对象。在C程序设计语言中,有3种类似于函数调用语法的实体:函数、类似于函数的宏和函数指针。由于函数和宏实际上并不是对象,因此在C语言中,我们只把函数指针看成仿函数。然而在C++中,还存在其他的函数对象:对于class类型,我们可以重载函数调用运算符;还存在函数引用的概念;另外,成员函数和成员函数指针也都有自身的调用语法。本篇在于把仿函数的概念和模板所提供的编译期参数化机制结合起来以提供更加强大的程序设计技术。仿函数的习惯用法几乎都是使用某种形式的回调,而回调的含义是这样的:对于一个程序库,它的客户端希望该程序库能够调用客户端自定义的某些函数,我们就把这种调用称为回调。------------------------------------------------------------------------------------------------------------22.1 直接调用、间接调用和内联调用

在阐述如何使用模板来实现有用的仿函数之前,我们先讨论函数调用的一些属性,也正是这些属性的差异,才真正体现出基于模板的仿函数的优点。

在博文“直接调用、间接调用和内联调用”中充分阐明了这里使用内联的优点:在一个调用系列中,不但能够避免执行这些(查找名称的)机器代码;而且能够让优化器看到函数对传递进来的变量进行了哪些操作。

实际上,我们在后面将会看到,如果我们使用基于模板的回调来生成机器码的话,那么这些机器码将主要涉及到直接调用和内联调用;而如果用传统的回调的话,那么将会导致间接调用。根据博文xxxx的讨论,可以知道使用模板的回调将会大大节省程序的运行时间。

22.2 函数指针与函数引用

考虑函数foo()定义:

extern "C++" void foo() throw() { }

该函数的类型为:具有C++链接的函数,不接受参数,不返回值并且不抛出异常。由于历史原因,在C++语言的正式定义中,并没有把异常规范并入函数类型的一部分。然而,将来的标准将会把异常加入函数类型中。实际上,当你自己编写的代码要和某个函数进行匹配时,通常也应该要求异常规范同时也是匹配的。名字链接(通常只存在于C和C++中)是类型系统的一部分,但某些C++编译器将会自动添加这种链接。特别地,这些编译器允许具有C链接的函数指针和具有C++链接的函数指针相互赋值。这同时带来下面的一个事实: 在大多数平台上,C和C++函数的调用规范几乎是一样的,唯一的区别在于:C++将会考虑参数的类型和返回值的类型。

在大多数上下文中,表达式foo能够转型为指向函数foo()的指针。即使foo本身并没有指针的含义,但是就如表达式ia一样,在声明了下面的语句之后:

int ia[10];

ia将隐含地表示一个数组指针(或者是一个指向数组第1个元素的指针)。于是,这种从函数(或者数组)到指针的转型通常也被称为decay。如下:

// functors/funcptr.cpp #include #include void foo() { std::cout (大于号)进行排序 std::set c3; // 用用户自定义的排序规则进行排序 ... c0 = c1; // 正确:相同的类型 c1 = c2; // 错误:不同的类型 ... if (c1 == c3) // 错误:不同的类型 { ..... } }

22.5 指定仿函数

在我们前面的例子中,我们只给出了一种选择set类的仿函数的方法。在这一节里,我们将讨论其他的几种方法。

22.5.1 作为模板类型实参的仿函数

传递仿函数的一个方法是让它的类型作为一个模板实参。然而类型本身并不是一个仿函数,因此客户端函数或者客户端类必须创建一个给定类型的仿函数对象。当然,只有class类型仿函数才能这么做,函数指针则不可以;而且函数指针本身也不会指定任何行为。另外,也不存在一种能够传递包含状态的类型的机制(因为类型本身并不包含任何特定的状态,只有对象才可能具有某些特定的状态,所以在此真正要传递的是一个特定的对象)。

下面是函数模板的一个雏形,它接收一个class类型的仿函数作为排序规则:

template void my_sort(... ) { FO cmp; // 创建函数对象 ... if (cmp(x, y)) // 使用函数对象来比较2个值 { .... } .... } // 以仿函数为模板实参,来调用函数 my_sort (... );

运用上面这个方法,比较代码(如std::less)的选择将会是在编译期进行的。并且由于比较操作是内联的,所以一个优化的编译器将能够产生本质上等价于不使用仿函数,而直接编写的代码。

22.5.2 作为函数调用实参的仿函数另一种传递仿函数的方法是以函数调用实参的形式进行传递。这就允许调用者在运行期构造函数对象(可能使用一个非虚拟的构造函数)就作用而言,函数调用实参和函数类型参数本质上是类似的,唯一的区别在于:当传递参数的时候,函数调用实参需要拷贝一个仿函数对象。这种拷贝开销通常是很低的,而且实际上如果该仿函数对象没有成员变量的话(而实际情况也经常如此),那么这种拷贝开销也将接近于0。如下:

template void my_sort(... , F cmp) { ... if (cmp(x, y)) // 使用函数对象,来比较两个值 { ... } ... } // 以仿函数作为调用实参,调用排序函数 my_sort(... , std::less());

22.5.3 结合函数调用参数和模板类型参数对于前面两种传递仿函数的方式——即传递函数指针和class类型的仿函数,只要通过定义缺省函数调用实参,是完全可以把这两种方式结合起来的:

template void my_sort(... , F cmp = F() ) { ... if (cmp(x, y)) // 使用函数对象来比较两个值 { ... } ... } bool my_criterion() (T const& x, T const& y); // 借助于模板实参传递进来的仿函数,来调用排序函数 my_sort (... ); // 借助于值实参(即函数实参)传递进来的仿函数,来定义排序函数 my_sort(... , std::less()); // 借助于值实参(即函数实参)传递进来的仿函数,来定义排序函数 my_sort(... , my_criterion);

22.5.4 作为非类型模板实参的仿函数我们同样也可以通过非类型模板实参的形式来提供仿函数。然而,class类型的仿函数(更普遍而言,应该称为class类型的对象)将不能作为一个有效的非类型模板实参。如下面的代码就是无效的:

class MyCriterion { public: bool operator() (SomeType const&, SomeType const&) const; }; template // ERROR:MyCriterion 是一个class类型 void my_sort(... );

然而,我们可以让一个指向class类型对象的指针或者引用作为非类型实参,这也启发了我们编写出下面的代码:

class MyCriterion { public: virtual bool operator() (SomeType const&, SomeType const&) const = 0; }; class LessThan : public MyCriterion { public: virtual bool operator() (SomeType const&, SomeType const&) const; }; template // class类型对象的指针或引用 void sort(... ); LessThan order; sort (... ); // 错误:要求派生类到基类的转型 sort(... ); // 非类型模板实参所引用的必须是一个简单的名称(不能含有转型)

在上面这个例子中,我们的目的是为了在抽象基类中描述这种排序规则的接口,并且在非类型模板实参中使用该抽象类型。就我们的想法而言,我们是为了能够在派生类(如LessThan)中来特定地实现基类的这种接口(MyCriterion)。遗憾的是,C++并不允许这种实现方法,在C++中,借助于引用或者指针的非类型实参必须能够和参数类型精确匹配,从派生类到基类的转型是不允许的,而进行显式类型转换也会使实参无效,同样也是错误的。

据此我们得出一个结论:class类型的仿函数并不适合以非类型模板实参的形式进行传递。相反,函数指针(或者函数引用)却可以是有效的非类型模板实参。

22.5.5 函数指针的封装本节主要介绍:把一个合法的函数嵌入一个接收class类型仿函数框架。因此,我们可以定义一个模板,从而可以方便地嵌入这种函数:

// functors/funcwrap.cpp #include #include #include // 用于把函数指针封装成函数对象的封装类 template class FunctionReturningIntWrapper { public: int operator() (){ return FP(); } }; // 要进行封装的函数实例 int random_int() { return std::rand(); // 调用标准的C函数 } // 客户端,它使用由模板参数传递进来的函数对象类型 template void initialize(std::vector& coll) { FO fo; // 创建函数对象 for(std::vector::size_type i=0; i::ResultT::Type Type; }; template class UsedFunctorParam { public: typedef typename F::Param1T Type; };

UsedFunctorParam是我们引入的一个辅助模板,对于每一个特定的N值,都需要对该模板进行局部特化,下面使用宏来实现:

// functors/functorparam2.hpp #define FunctorParamSpec(N) \ template \ class UsedFunctorParam{ \ public: \ typedef typename F::Param##N##T Type; \ } ... FunctorParamSpec(2); FunctorParamSpec(3); ... FunctorParamSpec(20); #undef FunctorParamSpec

22.6.3 封装函数指针

上面一小节,我们借助于typedef的形式,是仿函数类型能够支持某些内省。然而,由于要实现这些内省的约束,函数指针不再适用于我们的框架。我们可以通过封装函数指针来绕过这种限制。我们可以开发一个小工具,它能够封装最多具有2个参数的函数(封装含有多个参数的函数的原理和做法是一样的)。

接下来给出的解释方案将会涉及到2个组件:类模板FunctionPtr,它的实例就是封装函数指针的仿函数类型;重载函数模板func_ptr,它接收一个函数指针为参数,然后返回一个相应的、适合该框架的仿函数。其中,类模板FunctionPtr将由返回类型和参数类型进行参数化:

template class FunctionPtr;

用void值来替换一个参数意味着:该参数实际上并没有提供。因此,我们的模板能够处理仿函数调用实参个数不同的情况。因为我们需要封装的是函数指针,所以我们需要有一个工具,它能够根据参数的类型,来创建函数指针类型。我们通过下面的局部特化来实现这个目的:

// functors/functionptrt.hpp // 基本模板,用于处理参数个数最大的情况: template class FunctionPtrT { public: enum { NumParams = 3 }; typedef RT (*Type)(P1, P2, P3); }; // 用于处理两个参数的局部特化 template class FunctionPtrT { public: enum { NumParams = 2 }; typedef RT (*Type)(P1, P2); }; // 用于处理一个参数的局部特化 template class FunctionPtrT { public: enum { NumParams = 1 }; typedef RT (*Type)(P1); }; // 用于处理0个参数的局部特化 template class FunctionPtrT { public: enum { NumParams = 0 }; typedef RT (*Type)(); };

你会发现,我们还使用了上面这个(相同的)模板来计算参数的个数。

对于上面这个仿函数类型,它把它的参数传递给所封装的函数指针。然而,传递一个函数调用实参是可能会产生副作用的:如果相应的参数属于class类型(而不是一个指向class类型的引用),那么在传递的过程中,将会调用该class类型的拷贝构造函数。为了避免这个(调用拷贝构造函数)额外的开销,我们需要编写一个类型函数;在一般情况下,该类型函数不会改变实参的类型,而当参数是属于class类型的时候,它会产生一个指向该class类型的const引用。借助于在第15章开发的TypeT模板和熟知的IfThenElse功能模板,我们可以这样准确地实现这个类型函数:

// functors/forwardparam.hpp #ifndef FORWARD_HPP #define FORWARD_HPP #include "ifthenelse.hpp" #include "typet.hpp" #include "typeop.hpp" // 对于class类型,ForwardParamT::Type是一个常引用 // 对于其他的所有类型,ForwardParamT::Type是普通类型 // 对于void类型,ForwardParamT::Type是一个哑类型(Unused) template class ForwardParamT { public: typedef typename IfThenElse::ResultT Type; }; template class ForwardParamT { private: class Unused { }; public: typedef Unused Type; }; #endif // FORWARD_HPP

我们发现这个模板和前面的RParam模板非常相似,唯一的区别在于:在此我们需要把void类型(我们在前面已经说明,void类型是用于代表那些没有提供参数的类型)映射为一个类型,而且该类型必须是一个有效的参数类型。

现在,我们已经能够定义FunctionPtr模板了。另外,由于我们事先并不知道FunctionPtr究竟会接收多少个参数,所以在下面的代码中,我们针对不同个数的参数(但在此我们最多只是针对3个参数),都重载了函数调用运算符:

// functors/functionptr.hpp #include "forwardparam.hpp" #include "functionptrt.hpp" template class FunctionPtr { private: typedef typaname FunctionPtrT::Type FuncPtr; // 封装的指针 FuncPtr fptr; public: // 使之适合我们的框架 enum { NumParams = FunctionPtrT::NumParams }; typedef RT ReturnT; typedef P1 Param1T; typedef P2 Param2T; typedef P3 Param3T; // 构造函数: FunctionPtr(FuncPtr ptr) : fptr(ptr) { } // "函数调用": RT operator() (){ return fptr(); } RT operator() (typename ForwardParamT::Type a1) { return fptr(a1); } RT operator() (typename ForwardParamT::Type a1, typename ForwardParamT::Type a2) { return fptr(a1, a2); } RT operator() (typename ForwardParamT::Type a1, typename ForwardParamT::Type a2, typename ForwardParamT::Type a3) { return fptr(a1, a2, a3); } };

该类模板可以实现所期望的功能,但如果直接使用该模板,将会比较繁琐。为了使之具有更好的易用性,我们可以借助模板的实参演绎机制,实现每个对应的(内联的)函数模板:

// functors/funcptr.hpp #include "functionptr.hpp" template inline FunctionPtr func_ptr (RT (*fp) () ) { return FunctionPtr(fp); } template inline FunctionPtr func_ptr (RT (*fp) (P1) ) { return FunctionPtr(fp); } template inline FunctionPtr func_ptr (RT (*fp) (P1, P2) ) { return FunctionPtr(fp); } template inline FunctionPtr func_ptr (RT (*fp) (P1, P2, P3) ) { return FunctionPtr(fp); }

至此,剩余的工作就是编写一个使用这个(高级)模板工具的实例程序了。如下所示:

// functors/functordemo.cpp #include #include #include #include "funcptr.hpp" double seven() { return 7.0; } std::string more() { return std::string("more"); } template void demo(FunctorT func) { std::cout


【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3