function类模板
C++语言中有几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。
和其他对象一样,这些可调用的对象,也有自己的类型。
然而,两个不同的可调用对象,却有可能共享同一种调用形式。(调用形式指明了调用返回的类型以及传递给调用的实参类型)
1 |
int(int,int) |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//普通函数 int add(int i,int j) {return i+j;} //lambda表达式,产生一个未命名的函数对象类 auto mod = [](int i,int j) {return i % j;} //函数对象类 struct divide { int operator()(int denominator,int divisor) { return denominator/divisor; } }; //这三种不同的可调用对象,使用了相同的调用形式 |
对于上述这种几种不同的可调用对象共享一种调用形式的情况,有时候,我们希望把它们看成具有相同的类型。
function
定义在function头文件中
function |
f是用来存储可调用对象的空function,这些可调用对象的调用形式应该于函数类型T相同 |
function |
显示地构造一个空地function |
function f(obj) | 在f中存储可调用对象obj的副本 |
f | 将f作为条件:当f含有一个可调用对象时为真;否则为假 |
f(args) | 调用f中的对象,参数是args |
定义为function |
|
result_type | 该function类型的可调用对象返回的类型 |
argument_type | 当T有一个或两个实参时定义的类型。如果T只有一个实参,则argument_type是该类型的同义词;如果T有两个实参,则first_argument_type和second_argument_type分别代表两个实参的类型 |
first_argument_type | |
second_argument_type |
1 2 3 4 5 6 7 |
function<int(int,int)> f1 = add;//函数指针 function<int(int,int)> f2 = divide();//函数对象类的对象 function<int(int,int)> f3 = [](int i,int j) {return i*j;}//lambda f1(2,3); f2(2,3); f3(2,3); |
我们不能将重载函数的名字存入function类型的对象中。但可以用存储函数指针或者使用lambda表达式来解决这种二义性问题。
explicit类型转换运算符
C++11标准引入了显示的类型转换运算符,和显示的构造函数一样,编译器(通常)不会将一个显示的类型转换运算符作用于隐式类型转换。
1 2 3 4 5 6 7 8 9 10 11 12 |
class SmallInt { public: SmallInt(int i):val(i) { if(i < 0 || i > 255) throw out_of_range("bad samllint value"); } explicit operator int() const {return val;} private: size_t val; }; |
1 2 |
SmallInt si = 3;//正确,SmallInt的构造不是显示的,所以它会发送隐式转换 si + 3;//错误,此处需要隐式的类型转换,但类的运算符是显示的 |
虚函数的override指示符
派生类经常(但不总是)覆盖它继承的虚函数。
如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。
C++11标准,允许派生类显示地注明它使用某个成员函数覆盖了它继承的虚函数。具体做法是,在形参列表后面,或在const成员函数的const关键字后面、或者是在引用成员函数的引用限定符后面添加一个关键字override。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Base { public: virtual void f1(int) const; virtual void f2(); void f3(); }; class Derived:public Base { public: void f1(int) const override;//正确,重写了基类f1 void f2(int) override;//错误,基类没有形如f2(int)这样的函数 void f3() override;//基类f3不是虚函数 void f4() override;//基类里面没有f4这个函数,不能重写 }; |
防止继承的发生-final
有时候我们定义了一个类,但我们不希望别的类继承它,或者不想考虑它是否适合作为一个基类。
C++11标准提供了一种放置继承发生的方法,即在类名后面跟一个关键字final。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Base { public: virtual void f1(int) const; virtual void f2(); void f3(); }; class Derived_Other1:public Base { public: //从基类继承了f2和f3,覆盖了f1, void f1(int) const final;//final表示不允许后续的其他类覆盖f1(int) }; class Derived_Other2:public Derived_Other1 { public: void f2(); void f1(int) const;//这个就错误了 }; |
1 2 3 |
class NoDerived final {/**/}//不能做基类 class Base {/**/} class Last final:public Base {/**/}//Last不能再被继承 |
删除的拷贝控制和继承
继承关系对基类拷贝控制最直接的影响就是基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。
当我们delete一个动态分配的对象的指针时,将执行析构函数。
如果该指针指向继承体系中的某个类型,则有可能指针的静态类型与被删除对象的动态类型不符的情况。
而,如果基类的析构函数不是虚析构函数,那么我们delete一个指向派生类的基类指针,将会产生未定义行为。
我们之前的经验:
如果一个类需要析构函数,那么它同样需要拷贝和赋值操作。
但是,
有一个重要的例外,基类的析构函数不遵循这一点。
一个基类总是需要析构函数,而且,它能将析构函数设定为虚函数。而为了成为虚析构,令内容为空,这样一来,我们就无法由此推断该基类还需要赋值运算符或拷贝构造函数。
1 2 3 4 5 6 7 8 9 10 11 |
class quote { public: virtual ~quote() = default;//动态绑定析构函数 }; class bulk_quote:public quote { public: int i; int j; }; |
上面这个继承体系中,所有的类都使用合成的析构函数。其中(C++11标准),派生类隐式的使用而基类通过将其基类定义为=default而显示得使用。
合成的析构,函数体是空的。其隐式的析构部分,负责销毁类的成员。
对于派生类的析构函数来说,它除了销毁派生类自己的成员外,还负责销毁派生类的直接基类;该直接基类又负责销毁它自己的直接基类,以此类推至继承的最顶端。
派生类中删除的拷贝控制与基类的关系
就像其他任何类的情况一样,(C++11)基类或派生类也能出于同样的原因将其合成的默认构造函数或任何一个拷贝控制成员定义为被删除的函数。此外,某些定义基类的方式也可能导致有的派生类成员成为被删除的函数:
- 如果基类中的默认构造函数、拷贝构造函数、拷贝赋值运算符或者析构函数是被删除的函数或者不可访问。则派生类中对应的成员也将是被删除的。原因就是,编译器不能使用基类成员来执行派生类对象基类部分的构造、赋值或销毁操作。
- 如果基类中有一个不可访问或删除掉的析构函数,则派生类中合成的默认和拷贝构造函数是被删除的。原因是,编译器无法销毁派生类对象的基类部分。
- 派生类将不会合成一个删除掉的移动操作。当我们使用=default请求一个移动操作时,如果基类中对应的操作是删除的或不可访问的,那么派生类中该函数是被删除的。原因是,派生类对象的基类部分不可移动;同样,如果基类的析构函数是删除的或不可访问的,那么派生类的移动构造也将是被删除的。
继承的构造函数
在C++11标准中,派生类能够重用其直接基类定义的构造函数。尽管我们清楚,这些构造函数并非以常规的方式继承而来。
一个类只初始化它的直接基类,出于同样的原因,一个类也只继承其直接基类的构造函数。
类不能继承默认、拷贝和移动构造函数。
如果派生类没有直接定义这些构造函数,则编译器将为派生类合成它们。
派生类继承基类构造函数的方式是提供了一条注明了(直接基类)基类名的using声明语句。通常情况下,using声明语句只能令某个名字在当期作用域内可见,而当作用于构造函数时,using声明语句将令编译器产生代码。
1 2 3 4 5 |
class derived :public base { public: using base::base;//继承base的构造函数 }; |
编译器生成的构造函数形式如下:
derived(parms) :base(args) {}
1 |
derived(const string & a,double b,size_t c,double d):base(a,b,c,d) {} |
模板类型别名
类模板的一个实例定义了一个类类型,于任何其他类类型一样,我们可以定义一个typedef来引用实例化的类。
1 |
typedef Blog<string> strblog; |
由于模板不是一个类型,我们不能定义一个typedef引用一个模板。
但是,C++11标准,允许为类模板定义一个类型别名:
1 2 3 4 5 6 7 8 |
template<typename T> using twin = pair<T,T>; twin<string> author;//author的类型是pair<string,string> twin<int> t_i;//t_i的类型是pair<int,int> twind<double> t_d; template<typename T> using parNo = pair<T,unsigned>; parNo<string> books;//books的类型是pair<string,unsigned> parNo<Student> stu;//stu的类型是pair<Student,unsigned> |
声明模板类型形参为友元
在C++11标准,我们可以将模板类型参数声明为友元
1 2 3 4 5 6 |
template<typename T> class Bar { public: friend T;//将访问权限授予用来实例化Bar的类型 }; |
函数模板的默认模板参数
就像我们能为函数参数提供默认实参一样,我们也可以提供默认模板实参。
在C++11标准中,我们可以为函数或类模板提供默认实参。
1 2 3 4 5 6 7 8 9 |
template<typename T,typename F = less<T>> int compar(const T & lhs,const T & rhs,F f = F()) { if(f(lhs,rhs)) return -1; if(f(rhs,lhs)) return 1; return 0; } |
实例化显示控制
当模板被使用时才会进行实例化,这一特性意味着,相同的实例可能出现在多个对象文件中。
当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。
在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在C++11标准下,我们可以通过显示实例化来避免这种开销。
1 2 3 4 5 6 7 8 |
//实例化声明 extern template declaration; //实例化定义 template declaration; // extern template class Blog<string>;//声明 template int compare(const int&,const int&);//定义 |
当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明(定义)。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
函数模板与尾置返回类型
在编译器未遇到函数的参数列表之前,某些对象的相关信息是不存在的,为了定义这种类型的函数,我们可能需要使用尾置类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
template<typename T> ??? & func(T beg,T end) { //处理 return *beg; } //尾置类型实现 template<typename T> auto func(T beg,T end)->decltype(*beg) { //处理 return *beg; } |
引用折叠规则
1 2 3 4 5 6 7 8 9 |
template<typename T> void f3(T &&) { //do something } //对于 int i = 22; f3(i); |
对于上面的代码,我们可能认为f3(i)这样的用法是不对的,毕竟,f的参数,需要的是一个右值,而我们给它的,却是一个左值,通常情况下,我们不能把一个右值引用绑定到一个左值上。
但是,C++语言在绑定规则之外,定义了两个例外规则,允许这种绑定的发生(而这两个例外规则是move这种标准库设施正确工作的基础):
- 当我们将一个左值(i)传递给函数的右值引用参数,且此右值引用指向模板类型参数(T&&)时,编译器推断模板类型参数为实参的左值引用类型。(因此,当我们调用f3(i)的时候,编译器推断T的类型为int&,而不是int)
- 如果我们间接创建一个引用的引用,则这些引用形成了”折叠“。
在除第一种例外的所有情况下,引用会折叠成一个普通的左值引用类型。
在C++11标准中,折叠规则扩展到右值引用。
只有一种情况下,引用会折叠成右值引用:右值引用的右值引用。
对于一个给定的X:
- X& &、X& &&、X&& &、都折叠成X&
- 类型X&& && 折叠成X&&
引用折叠只能应用于间接创建的引用的引用,如类型别名或函数模板。
如果将引用折叠和右值引用的特殊类型推断规则组合一起,则意味着我们可以对一个左值调用f3。
首位,编译器推断T为一个左值引用类型:
f3(i);//实参是一个左值,模板参数是int&
f3(i);//实参是一个左值,模板参数是const int &
当一个模板参数是T被推断为引用类型时,折叠规则告诉我们函数参数T&&折叠为一个左值引用类型。
1 2 |
//无效代码,用于参考 void f3<int&>(int& &&); |
f3的参数是T&&且T是int&,因此,T&&是int& &&,会折叠成int &。因此,即使f3的函数参数形式上是一个右值引用(T&&),此调用也会用一个左值引用类型(int&)实例化f3:
1 |
void f3<int&>(int&); |
这两个规则导致了两个重要的结果:
- 如果一个函数参数是一个指向模板类型参数的右值引用(T&&),则它可以被绑定到一个左值;且
- 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个(普通)左值引用参数(T&)
用static_cast可以将一个左值转换为右值引用
通常情况下,static_cast只能用于其他合法的类型转换。
但是,这里又有一条针对右值引用的特许规则:
虽然不能隐式地将一个左值转换为右值引用,但我们可以用static_cast显示地将一个左值转换为一个右值引用。
标准库forward函数
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。
在此情况下,我们需要保持被转发实参地所有性质,包括实参类型是否是const的,以及实参是左值还是右值。
在C++11标准中,我们可以使用一个名为forward的标准库设施来传递函数的参数,它能保持原来实参的类型。
forward必须通过显示模板实参调用。
forward返回该显示实参类型的右值引用。即forward
通常情况下,我们使用forward传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward可以保持给定实参的左值/右值属性。
1 2 3 4 5 6 7 8 9 10 11 |
template <typename T> intermediary(T &&arg) { finalFunction(std::forward<T>(arg)); //... } template <typename F,typename T1,typename T2> void flip(F f,T1 && t1,T2 && t2) { f(std::forward<T2>(t2),std::forward<T1>(t1)); } |
可变参数模板
一个可变参数模板就是一个接收可变数目参数的模板函数或模板类。
可变数目的参数被称为参数包
存在两种参数包:
- 模板参数包(表示0个或多个模板参数)
- 函数参数包(表示0个或多个函数参数)
我们用一个省略号来指出一个模板参数或函数参数表示一个包。
在一个模板参数列表中,class...或者typename...指出接下来的参数表示零个或多个类型的列表;一个类型后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。
在一个函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。
12345//Args是一个模板参数包,rest是一个函数参数包//Args表示0个或多个模板类型参数//rest表示0个或多个函数参数template<typename T,typename...Args>void foo(const T & t,const Args&...rest);另外,当我们需要知道包中有多少个元素时,我们可以使用sizeof(这也是C++11标准的一点)。
sizeof...也返回一个常量表达式,而且不会对其实参求值。123456template<typename...Args>void g(Args...args){cout << sizeof...(Args) << endl;//类型参数的数目cout << sizeof...(args) << endl;}
可变参数模板与转发
在C++11标准下,我们可以组合可变参数模板与forward机制来编写函数,实现将其实参不变地传递给其他函数。
1 2 3 4 5 6 7 8 9 10 11 12 |
class str { public: template<class...Args> void emplace_back(Args&&...); } template<class...Args> inline void str::emplace_back(Args&&...args) { chk_n_alloc(); alloc.construct(first_free++,std::forward<Args>(args)...); } |
这么这样的写法,既扩展了模板参数包Args,又扩展了函数参数包args。
生成:std::forward<Ti>(ti)
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ C++_指针引用09/19
- ♥ C++并发编程 _管理线程05/07
- ♥ COM组件_101/31
- ♥ STL_内存处理工具05/02
- ♥ STL_queue06/07
- ♥ Soui一03/17