C++

您所在的位置:网站首页 pre模板是啥 C++

C++

2023-10-10 05:00| 来源: 网络整理| 查看: 265

模板是一个非常强大的C++功能,STL的各种组件也是基于模板的。所以,无论是写程序了,还是读程序,都有必要了解一下C++的模板。

关于什么是模板或者模板的基本定义,这里就不讲述了,本篇文章主要罗列出在使用模板过程中的一些问题和模板一些令人头疼的语法,并配合简单的demo,如果你只是希望查阅语法或者了解一些知识点,这篇文章可能会帮到你。

声明:使用了using namespace std。对于应该包含进来的头文件,不再显示的声明。文中所有demo均经过测试。本文章基于《C++ Primer Plus》和《C++ Prime》。

目录

模板的基本声明和定义

模板的声明

定义一个模板函数

定义一个模板类

模板参数作用域

关于模板工作原理

非类型模板参数

inline和constexp

 在模板类中使用模板类

 友元与模板类

默认模板实参

: : 二义性的解决

类模板成员函数

类模板的成员模板 

实例化优化 

类型转换和参数推断

返回值类型推断

兼容类型的模板问题

函数指针实参推断

模板实参推断

左值引用

 右值引用

引用折叠

std::move

短小精悍的std::move定义

std::move的解析

模板函数匹配的特殊性

注意重载模板声明顺序 

模板特例化

特例化和重载 的区别

类模板特例化 

部分模板特例化

特例化成员

可变参数模板

可变模板参数的具体作用

模板技巧

转发 

使用std::forward 

转发参数包 

make_shared的工作原理

模板的基本声明和定义 模板的声明 template int compare (T t1, T t2); template class compare; 定义一个模板函数 template int compare(T & t1, T & t2) { if(t1 > t2) return 1; if(t1 == t2) return 0; if(t1 < t2) return -1; } 定义一个模板类 template class compare { private: T _val; public: explicit compare(T & val) : _val(val) { } explicit compare(T && val) : _val(val) { } bool operator==(T & t) { return _val == t; } }; 模板参数作用域

就如同其他的函数参数一样,或者是变量一样,就是普通的作用域规则。

using T = int; T a = 10; template class A;

 模板声明里的T不是上面的int而是模板参数。

template class A { U val; //error template class B; }; 关于模板工作原理

模板定义并不是真正的定义了一个函数或者类,而是编译器根据程序员缩写的模板和形参来自己写出一个对应版本的定义,这个过程叫做模板实例化。编译器成成的版本通常被称为模板的实例。编译器为程序员生成对应版本的具体过程。类似宏替换。

模板类在没有调用之前是不会生成代码的。

由于编译器并不会直接编译模板本身,所以模板的定义通常放在头文件中。

非类型模板参数

顾名思义,模板参数不是一个类型而是一个具体的值——这个值是常量表达式。

当一个模板被实例化时,,非类型参数被一个用户提供的或者编译器推断出的值所代替。正因为模板在编译阶段编译器为我们生成一个对应的版本,所以其值应该能够编译时确定,那么他应该是一个常量或者常量表达式。

有一句话说:C++的强大在于他的编译器强大,下面这个例子就是很好的说明。

template int str_compare(const char (&str1)[N], const char (&str2)[M]) { return strcmp(str1,str2); }

 使用方法

str_compare("hello","nihao")

为什么???我们甚至没有用来传递模板参数。这是因为编译器在编译阶段已经帮助我们计算好了应该开辟多大空间的数组。我们也可以指定长度。N,M只是隐式的传入进去。

编译器也可以自动帮助我们推断参数时什么类型,从而不用显示的调用模板函数,对于上面的compare函数,我们可以这样调用,前提时保证参数类型相同。

compare(10,20);

非类型模板参数的范围

整形,指针或者左值引用都是一个非类型模板参数。

我们可以想到,对于指针或者引用,应当保证实参必须具有静态的生存期,保证其不会被释放。

inline和constexp

放在模板之后,函数之前即可

template inline int compare(T t1, T t2);  在模板类中使用模板类

这个应该很好理解,根据自己的需求,我们可以这样定义

template class A { private: vector vec; };

 也可以这样定义

template class B { private: vector vec; };  友元与模板类

通过上面编译器为模板生成具体代码的原理可以看出这样有什么不同

template class C friend A; friend B;

 由于具体的原理类似宏替换,每个对应的C都有友元A和B、

即有这样友元关系C A B, C A B以此类推。

还有这样的模板友元——所有的实例化都是其友元

template class C template friend class D;

 但是没有这样的写法

template friend class D;

或者这样的写法

template friend D;

模板允许模板参数为自己的友元

首先说明,模板允许内置类型为自己的友元。

friend int;

这样写是完全正确的,但是实际上有什么意义呢?

还是有意义的,我们可以这样写

template class People { friend T; };

这样就保证了在传入内置类型的时候不会有错误。

默认模板实参

用法和函数的默认参数基本相同

template class A;

默认的情况下T就是int

A a; // T is int : : 二义性的解决

对于普通类的:: ,我们可以知道它究竟是一个类还是一个静态成员,就像下面这样。

string::size_type a; string::npos;

对于模板类来说,我们还是知道表达的是什么,但是已经说过了,模板类在没有 调用之前不会生成代码,这可坏了。对于T::mem,究竟是什么呢?是静态成员?还是一个类型的typedef?

对于这个问题,使用typename修饰。

当我们希望通知编译器一个名字表示一个类型时,使用且必须使用关键字typename,来表示其是一个类型。

于是,我们可以写出这样的代码。

template typename T::val_typefunc ();

表的不是一个静态数据成员而是一个类型。

或者这样的代码

typedef typename T::mem s_type;

表示s_type是一个类型的别名而不是数据成员的别名。

如果转到string::size_type的定义,可以看见他是一个typename 的 typedef。

类模板成员函数

本质上就是个函数,只要掌握了模板的工作原理,我们我们就可以轻松的写出类模板成员函数。

class Math { public: template inline static N sqrt(N); }; template N Math::sqrt(N val) { return val * val; }

首先来一点一点解析

这是一个模板函数,返回值为N类型,所以,模板语法写在前面,让编译器知道应该返回类型,紧接着就是返回类型,返回类型同上都是写在比较靠前的位置。接着就是函数的标签。

对于定义来说,应该知道是哪个类下的函数,所以和普通的方法一样加上一个作用域即可。

假如把类写成这样呢?

template class Math { public: inline static N sqrt(N); };

那方法的定义应该是写成这样的。

template N Math::sqrt(N val) { return val * val; }

这里就可以看出

前面说到的,模板不是一个具体的类,而是根据这个模板编译器生成对应的版本。

对于每一个版本,都是不同的类。就像重载函数一样,即便参数个数和函数的具体算法完全一样,但类型不同他们也是不同的函数,只不过函数名相同而已。

那么就应该可以得到每个版本的类都对应的一个相应版本的静态成员。所以Math::这样写也就很好理解了。

类模板的成员模板 

我已经不知道用什么语言来下面的代码了。但是我们知道了一些事情。

无论是定义还是声明,模板语法的优先级是最高的,不同模板的优先级又根据其声明顺序来判断,其次是函数修饰,然后是返回值。根据这个原则我们可以轻松的解析这个函数。 

template class A { public: template A sum(It _begin, It _end); }; template //最外层模板 template //内层模板 A //返回值 A::sum(It _begin, It _end)//函数标签 {} //算法实现 //不妨写的更美观一点 template template A A::sum(It _begin, It _end){ }

注意:上面的代码和下面的代码写的足够复杂,下面的代码对其进行一些小小的修改。 

具体的用法,虽然下面的例子看起来有些造作,但是还是能说明一些问题的

#include #include #include using namespace std; template class A { private: vector vec; public: template T sum(It _begin, It _end); A(initializer_list initlist) { for(auto it = initlist.begin();it != initlist.end();++it) { vec.push_back(*it); } } typename vector::iterator begin() { return vec.begin(); } typename vector::iterator end() { return vec.end(); } }; template template T A::sum(It _begin, It _end) { T tot ; memset(&tot,0,sizeof (T)); while(_begin != _end) { tot += *_begin++; } return tot; } int main() { A a {1,2,3,4}; cout decltype(*_beg) { //... auto sum = *_beg; sum = 0; for_each(_beg,_end,[&sum](const int & val){ sum+= val;}); //... return sum; }

这样的代码还是有一些问题的,如果我们要返回一个拷贝而不是引用呢?要用到一个类型转换模板工具。

remove_reference        移除引用——关于其他的类型转换,不再本文章讨论范围内读者可自行查阅。

这个模板类有一个名为type的public成员,能够获得相应的类型。所以我们可以这样写

template auto func(It & _beg, It & _end) -> typename remove_reference::type //don't forget typename { //... auto sum = *_beg; sum = 0; for_each(_beg,_end,[&sum](const int & val){ sum+= val;}); //... return sum; }

在某些情况下我们可以指定返回u类型,例如

template T1 sum(T2 t2, T3 t3) { return t2 + t3; } sum(10,200); //or sum();

显示模板参数按从左到右的顺序一次匹配。

兼容类型的模板问题

有这样的代码

template T sum(T t1, T t2) { return t1 + t2; } sum(10,3.14);

虽然int和double兼容,但是只有一个类型参数,编译器傻了,T为int?精度会丢失,肯定是不可行的,T为double?貌似也不行,这样会导致数据溢出。无奈我们只好这样了。

template ??? sum(T1 t1, T2 t2) { return t1 + t2; }

至于返回类型,全交给程序员来规定,或者用尾部返回类型。

函数指针实参推断

有趣的是,虽然在未实例化之前,编译器没有生成具体的代码,但我们仍然可以进行函数指针绑定的操作。

template int compare(const T & t1, const T & t2) { } int (*pf_int)(const int &,const int &) = compare;

同样的我们也可以将模板函数作为回调函数进行传参,但此时可能会产生二义性,所以注意显示的写出模板参数。

当参数是一个函数模板实例的地址时,程序上下文必须满足:对于每个模板参数,能唯一确定其类型的值。

模板实参推断

这里是重中之重!!!重中之重!!!

很多的模板问题都与此有关。

关于const和&的问题,我们上面已经讲过了。这里再进行进一步的说明。

左值引用 template void func1(T &) { } template void func2(const T &) { } void aa() { int a = 10; const int b = 20; func1(a); //T is int func1(b); //T is const int func2(a); //T is int func2(b); //T is int func2(10); //T is int }

还是比较有意思的,看func2(b)的调用,虽然我们将const int类型传入进去,但是编译器为我们推导的还是int,原因应该和参数类型有关,如果编译器为我们推导的是const int ,那么const const int是不合法的,所以只好为我们推倒为int,即使我们调用时候的类型是const int。

 右值引用 template void func(T &&); func(10); //T is int func(b); //b is a left_val T is ???

我们可以根据引用折叠可以推断出类型。

引用折叠和万能引用

众所周知在非模板函数中可以使用const & 来接受任意类型参数,在模板中,也有类似这样的万能引用,就是&&。知道了这样的原因是有着引用折叠得的存在。

先说结论:在传递参数的过程中,无论多么复杂的引用传参,最后都会被折叠为& 或者 &&.

如果我们间接创建了一个引用的引用,则这些引用形成折叠。除了右值引用的右值引用会被折叠为一个右值引用,剩下全部折叠为一个左值引用。即

T& &, T& &&, T&& &都会折叠为T&

T&& &&会被折叠为&&

这就意味着我们 可以解释上面的问题。

当我们将一个左值传递给一个右值引时候,编译器推断T的类型为&。注意是T的类型为左值引用,不是整个形参是T &。

所以

func(b); //b is a left_val T is int&

上述的两个规则导致了

如果一个函数参数是一个指向模板类型参数的右值引用,则他可以被绑定到一个左值。

如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数将被参数将被实例化为一个普通左值引用参数。

 这两个规则又暗示了我们——我们可以将任意类型的实参传递给参数为右值引用的函数。

当代码中涉及的类型可能是非引用类型,也可能是引用类型的时候,编写正确的代码就变得异常困难(虽然remove_reference这样的转换类型对我们可能有所帮助)。

PS:由于这里的知识是在是很乱,笔者在写这里的时候也实在无能为力,所以大量了引用C++ Primer的原文。但是有一点可以保证——笔者在这里写的demo虽然没有什么实际意义仅用于演示——但是也能说明一些问题。

如果读者对模板的细节想以探究经,可以翻越C++ Primer——中文第五版P508-P610。

如果想巩固这里的语法,可以作相应的配套习题。

std::move

折磨的篇章终于过去了,让我们用好奇心来看一看std::move这个工具。

短小精悍的std::move定义

如下

template typename remove_reference::type && move(T && t) { return static_cast(t); } std::move的解析

因为move可以接受任意对象,所以应当是一个模板类。

既然我们要保证返回一个右值,那我们应当明确的得到一个非左右值引用类型——即普通类型。

那么就可以先移除引用再加上右值引用——这样保证了返回一个右值引用对应了

typename remove_reference::type &&

既然接受任意一个对象,那美可以用&&来接受实参,对应

move(T && t)

我们只需要返回一个右值即可,所以只有一个return语句。

我们回想以下为什么要使用std::move——获得一个右值进行移动构造?又或者是仅仅需要一个右值?不管出于什么原因,最终的目的就是为了优化程序,所以通过形参创建一个额外的右值并返回这样是不可取的,是脱裤子放屁,所以我们要使用这条语句

static_cast(t);

通常情况下,static——cast用于合法的类型转换,但是又一种情况例外,虽然一个左值不能隐式转换为右值,但是可以使用static_cat将其显示的转换为右值——前提我们先移除对象身上的引用效果。

模板和重载

模板也是可以被重载的,只要没有二义性。像在C++库中,存在着大量的模板重载技术,或者是可变模板参数中,也存在着模板的重载。

对于实例化的选择,遵循以下的规则。

1.对于一个调用,其候选函数是所有可行的实例化

2.可行函数按类型转换来排序。当然,可用于函数模板调用和的类型转换是非常有限的。

3.和普通函数一样,如果恰又一个函数比任何其他函数都更好的匹配,则选择此函数。

4.如果多个函数提供了同样好的匹配

        1)优先选择非模板函数

        2)没有非模板函数我选择更加特例化的模板

        3)否则有二意性

正确的定义一组重载的函数模板需要对类型键的关系以及模板幻术允许的优先的实参类型转换有着深刻的理解。

注意:虽然非模板函数的优先级很高——但那也是没有对应模板匹配的情况下,所以,在重载模板的时候仔细观察和思考。

所以我们为了适配字符串的比较,可以写出这样的代码

template int compare(const char str1[N], const char str2[M]) { return strcmp(str1,str2); } //或者 int compare(const char * const str1, const char * const str2) { return strcmp(str1,str2); }

 根据上面的匹配规则,我们还可以递归的调用模板类自己实现某些功能

template string debug_rep(const T & t) { ostringstream ret; ret


【本文地址】


今日新闻


推荐新闻


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