文章目录
- 1. 可变模板参数
- 2. Space in Template Expressions
- 3. nullptr and std::nullptr_t, auto
- 4. Uniform Initialization
- 5. Initializer Lists
- 6. explicit for ctors taking more than one argument
- 7. range-based for statement
- 8. =default, =delete
- 9. Alias Template
- 10. Template template parameter
- 11. Type Alias, noexcept, override, final
- 12. decltype
- 13. lambdas
- 14. Variadic Templates 1
- 15. Variadic Templates 2
- 16. Variadic Templates 3
- 我的补充
- 17. Variadic Templates 4
- 18. Variadic Templates 5
- 19. Variadic Templates 6
- 20. Variadic Templates 7
1. 可变模板参数
这在面向对象的课程就讲过了。
值得注意的是,2与3是可以并存的。在VS2019中测试如下:
可以看到都是调用第一个,说明
template<typename T, typename... Types>
void print(const T& firstArg, const Types&... args)
更加特化,而另一个更泛化,优先调用更特化版本的。
下面两张图为标准库的一些片段,可以看到一些调用流程,学习一下:
2. Space in Template Expressions
其实就是支持现在常见的写法了,之前一定要加空格。
3. nullptr and std::nullptr_t, auto
新增nullptr,见下图:
4. Uniform Initialization
一致性初始化:现在新特性下可以统一用 {} 来初始化
上图有深入的实现的理解。
5. Initializer Lists
如第四点那张图,编译器看到大括号 {t1, t2, …, tn} 就把他们看作一包(initializer_list)
上面那样设初值需要initializer_list类来实现,这在标准库的实现里有很大的影响。
如上图,P s={77, 5}; 也是调用构造函数。
一包(initializer_list) 可以接受任意个数。
如果没有版本2,那么在P q{77, 5}; 就会调用1,这时候编译器会把参数拆开,一个一个传给构造函数(而在之前则是直接看作一包!)。同样s也可以,但是r就编译不过去(有3个参数,但构造函数只有两个)。
而他的内部是array实现的:
回过来看:
如上可见其在标准库所占的成分,影响很大。
并且通过这样的方式,我们可以实现min和max函数一次传一包参数。
6. explicit for ctors taking more than one argument
2.0版本之前:
explicit主要用在构造函数之前。这在之前的面向对象下有讲过。
2.0之前只有单一实参可以。
2.0之后:
7. range-based for statement
编译器解释如下:
如下图加了explicit就不能这样用(因为赋值时要做转换,但explicit不允许):
8. =default, =delete
这两个关键词主要用在构造函数、拷贝构造、拷贝赋值等函数上,这些特殊的函数也叫Big-Three,而当2.0之后多了右值引用,也叫Big-Five,如下图:
C++2.0之前的复习:
即空的class编译器会自动声明这些默认版本的函数。且都是inline的。
只要这个类带有pointer-member,基本就要写Big-Three,而不带则一般不需要写。
比如上图复数类的实现没有指针成员,则没有Big-Three,而string有:
再往下看:
=delete告诉编译器不要定义他。
而像NoCopy这样子太绝对了。现实中我们还可以开一个后门,把拷贝构造、拷贝赋值写在private里:
这样就只有friends和members可以去copy。
9. Alias Template
化名:
如上图,可以把vector指定自己的分配器,而用别名就避免了每一次使用都要写那么长。
而使用宏则无法达到相同效果。
但是使用化名无法对化名做特化。(化名无法代表本尊)
本意想写个函数测试容器可搬不可搬(move),来测试他们的时间(这里没写出来),但是放进来的参数一定是object,怎么能拿他的type去做文章呢?简直是天方夜谭。
后来改动如上图右边,一个typename后面加()表示临时的变量,想拿这样的对象拿来用,但是Container怎么确定是一个模板呢,仍然是天方夜谭。
再次改动仍然不认识:
最后改动:
想要得到容器内部的类型需要萃取机traits。可以办到,但还是难以实现原本的接口。
但要是还是不死心要像之前一样弄出接口如(list, MyString)呢?下面引出模板模板参数以及化名的用法:
10. Template template parameter
上图事实上有点不好理解(class那里省略了T),可以看一下之前面向对象下的课件:
然而这样使用仍然会报错。
观察标准库源码,知道vector模板第二个参数有默认值,而在模板模板参数无法推出来。
解决方法:
11. Type Alias, noexcept, override, final
其实Type Alias类型别名就类似typedef:
using的用法:
在某一些class里头写如using _Base::_M_allocate;那么之后就可以只写_M_allocate而不用加原来的类::
在某条件下一定不会发出异常:
这里void foo () noexcept; 的后面的条件默认为true,即一定不会发出异常。
异常如果抛出没有被处理,就会一直往源头去抛,比如A调用B,B抛出异常,但A没处理,就会继续往调用A的地方去传递。要是一直没有处理异常程序就会崩溃。
如上,通过通知C++noexcept告诉上层vector可以用搬移的函数。(只有vector会成长grows)避免copy,效率提升。
override需要函数签名完全相同。这个关键字帮助编译器检查。(把要重载的心意告诉编译器)
final:第一种用法:
修饰class,告诉别人不能被继承
修饰函数function,告诉别人(比如继承过来)不能复写
12. decltype
得到对象的type:
可以把这个关键字想象成typeof:
有三种用法,我们分别来看一下:
- 让编译器通过。
在模板中不知道真正的x + y是什么类型,比如苹果类 + 梨子类,可能得到的是水果类(向上转型了),但是我们不知道究竟是什么。
但是简单的像上面这样写是通不过的,我们要使用auto关键字,结合decltype指定方式。有点点像lambda表达式。
- 适用于元编程(metaprogramming,不要想的太严肃了,先想成就是在函数模板里用)
只要用到了T::如上图的 T::iterator,就要加typename,以显示地告诉编译器T::iterator是一个类型名。
而由于模板只是一个半成品,用的时候如图传入一个复数的临时对象,但是复数类不是容器没有iterator,就会报错。 - lambda
lambda的type是很难写出来的,一般都是直接auto,decltype传入容器当作typeof。
13. lambdas
下图中间的那一段的小括号就不是产生临时对象了,而是执行,但是一般我们会按下面那一段去做:
mutable(中括号捕获的值可否改变), throwSpec(一个函数可不可以抛出异常), retType(返回类型) 三者有一个就要写():
注意看下图左侧的结果,传入的id不是按引用传递,所以不会影响外头的id:
在上图中,如果没有写mutable,就不能在里头id++。
再来看下图,传引用就会改变外部的id,而传值又不为mutable就会报错:
又回到了decltype的用法,用来传递lambda的type:
由于lambda的语法,如法给他构造函数或是赋值操作。所以必须要把lambda传给构造函数(如上的cmp传递给coll容器),否则就会调用默认构造函数,然后就会报错。
所以得如上这样写出:
std::set<Person, decltype(cmp)> coll(cmp);
14. Variadic Templates 1
重新回到开头:
七个大例子:
15. Variadic Templates 2
用Variadic Templates重写printf(),很简单就实现了出来:
16. Variadic Templates 3
参数type相同的话用initializer_list就可以了:
但是这样写max的参数必须要用{}来产生临时对象,变成initializer_list。
我的补充
这里我查看cplusplus网站:
https://www.cplusplus.com/reference/algorithm/max/?kw=max
可见max有三种(这里我们以C++11的标准来看,14和11差不多),默认为两个参数,即我们平常使用的
std::cout << std::max(1, 4) << std::endl;
第一种其实不用包含头文件algorithm也能用。
而第二种和第三种则必须要包含头文件algorithm。
而第二种则是可以给一个比较方法。我们先来读读标准库源码(测试环境VS2019):
可以看到,标准库首先用了noexcept关键字(这在前面说过,如果为true则代表一定不会抛出异常)来让程序更健壮(后面注释也说了strengthened)。
然后可以看到模板参数的_Pred其实就是一个仿函数,如果为真则这个max结果返回_Right,为假则返回_Left,我写的测试程序如下:
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
pair<int, int> FirstArg = { 1, 10 };
pair<int, int> SecondArg = { 100, 2 };
pair<int, int> DefaultResult = max(FirstArg, SecondArg);
pair<int, int> CompResult = max(FirstArg, SecondArg, [](pair<int, int>a, pair<int, int>b) {return a.second < b.second; });
cout << "DefaultResult: " << DefaultResult.first << " " << DefaultResult.second << endl;
cout << "CompResult: " << CompResult.first << " " << CompResult.second << endl;
return 0;
}
程序运行结果:
可以看到,默认的是比较pair的第一个数的大小,如果第一个数相等则再比较第二个数。所以返回的是SecondArg.
而使用max的第二种方法,我们给了一个lambda表达式,用来比较两个pair的second,如果第一个的second比第二个的大就返回false,false则对应返回_Left,即这里返回的是FirstArg.达到了比较pair第二个参数的目的。
第三种方法即传入多个参数,必须要用{}包起来,测试程序如下:
#include <iostream>
#include <algorithm>
using namespace std;
int main()
{
cout << max({ 1, 2, 3, 4, 5 }) << endl;
return 0;
}
多个参数的时候必须要用{}包起来,实际上在这里编译器会将其推导为initializer_list类型,产生一个initializer_list的临时对象,然后会调用标准库中的(algorithm第9545行):
这里的_STD实际上就是::std::,接着则会调用:
可以看到,之后就是简单地在initializer_list中找到最大值就可以了。简单提一句,initializer_list的底层就是array去实现的。
17. Variadic Templates 4
18. Variadic Templates 5
19. Variadic Templates 6
前面的例子都是通过分解Args去递归调用函数。
这里用类模板来用于递归继承:
这里使用private继承,非常好:
这里正规来讲只要在这里写了::(Head::type)就要在前面写上typename,不过有的编译器不写也能过。
而这里的tuple的构造函数,除了设定m_head,还调用了父类的构造函数:inherited(vtail…)
虽然看起来逻辑很正确,却在Head::type报了错:
因为这里的Head是int、float、string,怎么能回答type呢?
所以改为:
不过其实直接拿Head来用就行了…:
20. Variadic Templates 7
之前讲Variadic Templates用于递归继承,这里讲Variadic Templates用于递归复合:
这里Variadic Templates所有的例子都是递归:递归调用、递归创建、递归继承、递归复合