C++——C++11新语法(中)
一、引用折叠
在C++中不能直接定义引用的引用,比如说int& && r = i;直接这样定义是会出现报错的,但是可以通过模版或者typedef来构成引用的引用。C++制定的规则是右值引用的右值引用是右值引用,其他的组合方式均会被折叠成左值引用,看看以下的案例:
lref表示一个左值引用的类型,rref表示右值引用的类型,因为引用折叠的存在所以r1的类型是左值引用,r2的类型也是左值引用,r3的类型还是一个左值引用,只有r4是右值引用的右值引用,所以只有r4的类型是右值引用。
int main()
{
typedef int& lref;
typedef int&& rref;
int n = 0;
lref& r1 = n;
lref&& r2 = n;
rref& r3 = n;
rref&& r4 = 0;
}
在以前我们在编写一个函数的形参的时候,一般都会编写成一个const左值引用的版本,但是有了引用折叠以后,我们就可以搞出一个万能引用的模版。当我们给这个函数传入一个左值或者左值引用的实参时,编译器就会自动帮我们生成一个左值引用版本的Function函数,此时T的类型是左值引用;当我们给Function传入一个右值时,此时编译器会生成一个右值引用版本的Function函数,但是需要注意的是,此时对应的T的类型并不是右值引用类型的,而是这个类型本身的类型。也就是说,比如我给Function传入了一个0,那么此时被实例化出来的T类型是int,并不是int&&。
template<class T>
void Function(T&& t)
{
}
我们之前也说过,右值引用本身的属性也是一个左值,如果此时我们给函数传入了一个右值,此时这个数据在这个函数里的属性就变成了左值,当我们想要把这个数据按照原有的属性再传递给其他函数时,此时就需要借助到一个叫完美转发的东西。
完美转发forward的本质是一个函数模版,他主要还是通过引用折叠的方式实现,假如我们传入的参数是一个右值,此时这个类型不会发生折叠,被推导为原类型,forward内部就会把他强制类型转化为右值引用返回;如果传入的是左值,此时发生了折叠,forward内部就把参数强制类型转化为左值引用返回。
二、可变模版参数
C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:模板参数包,表⽰零或多个模板参数;函数参数包:表⽰零或多个函数参数。template <class ...Args> void Func(Args... args) {}。
这里的...是我们表示这是一个模版参数包或者函数参数包,这些函数参数包同样可以用左值引用和右值引用表示,同时和普通的模版参数一样,每个参数实例化时都遵循引用折叠的规则。
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。本质上就是让编译器识别到底有多少个模版参数,然后再让编译器自动生成出对应版本的模版函数。
我们还可以用sizeof...()操作符来计算参数包里的参数个数。
包扩展
一个参数包我们除了可以计算里面的参数个数,我们还有扩展它,也就是解析它,这里用到的解析的方式有点奇怪。
在没有可变模版参数时,如果我们实现这两个函数,那我们至少得去实现两个参数个数不同的模版函数,但是有了可变模版参数以后,不管有几个参数,我们只需要往里传就行,我们只要做好包扩展就能解析出参数里的数据。
这里解析的方式有点像函数的递归调用我们这里的ShowList有两个版本,一个是没有参数的特化版本,一个是有一个模板参数和一个可变模板参数的版本,此时Print把可变模板参数传给ShowList时,此时参数包里的个数如果是大于1,就会去调用第二个版本的ShowList,把参数包里的第一个参数给实例化了,然后继续递归的调用,当参数包里没有参数的时候,就会调用到ShowList没有参数的版本,此时就不会继续调用了,此时这个没有参数的ShowList版本就行递归函数的递归出口。
void ShowList()
{
cout << endl;
}
template<class T, class ...Args>
void ShowList(T t, Args... args)
{
cout << t << " ";
ShowList(args...);
}
template<class ...Args>
void Print(Args... args)
{
ShowList(args...);
}
int main()
{
Print(1, "123", 1.2,string("weadasd"));
Print(1, 1.2,string("weadasd"));
return 0;
}
这里还有第二种的包扩展方式,这里的这种方式就是编译器在编译时会把args实例化成对应数量的类型的参数,那么Argument的参数类型也是一个args,它也会被实例成一样的参数类型和个数,此时GetArg只能接受一个参数同时返回一个参数,那么此时编译器为了让参数类型匹配就会让GetArg在这个地方展开,此时我们就可以拿到参数包里的每个参数了。在这里这个Argument这个函数的意义就是为了让编译器让GetArg在这里展开,作用仅仅是接收GetArg的返回值即可。
template<class ...Args>
void Arguments(Args... args)
{}
template<class T>
const T& GetArg(const T& t)
{
cout << t << " ";
return t;
}
template<class ...Args>
void Print(Args... args)
{
Arguments(GetArg(args)...);
}
三、类的新功能
1、新的默认成员函数
在C++11之前类有六个默认的成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const 取地址重载。默认成员函数就是我们不写的时候编译器会默认生成一个。C++11中新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
如果你没有自己实现移动构造,切没有实现析构函数、拷贝构造、拷贝赋值重载,那么编译器才会自动生成一个默认的移动构造。这个默认的移动构造对于内置类型的会逐字节的进行拷贝,对于自定义类型的成员则需要看这个成员是否实现了移动构造,如果实现了移动构造就会调用它的移动构造,如果没有实现则会调用拷贝构造。
生成默认的移动赋值的条件和默认移动赋值的功能和上面的移动构造是完全类似的。
如果你提供了移动构造和移动赋值,编译器就不会自动提供拷贝构造和拷贝赋值了。
这里我认为这样这样设计的原因是,如果你没有主动的实现析构函数、拷贝构造和拷贝赋值,那么说明你认为这个类是一个只需要浅拷贝的类,但是这个类里面的成员变量可能有需要进行深拷贝的,所以默认生成的会去里面调用对应的移动构造或者移动拷贝。
2、声明时给缺省值
在声明类的成员变量时可以给定缺省值,在使用构造函数中,如果没有使用初始化列表来初始化这个成员变量,那么就会用给定的缺省值初始化这个成员变量。
3.default和delete
在C++11中,如果我们要强制让编译器生成一些默认的函数,那么我们可以用default关键字显示制定生成。
同时在C++98中,如果有一些默认生成的函数我们不想被别人使用,那么我们对应的方法是把这个函数设置成private,同时在只声明不实现,这样其他人在调用的时候就会报错了。在C++11中我们只需要在这个函数的声明后面加上=delete即可,这样编译器就不会生成这个函数的默认版本。
4、final和override
C++98中,如果我们不想这个类被继承,那么我需要做的是把这个类的构造设置成private,派生类在构造的时候一定要调用基类的构造函数,但是我们把它设置成了private以后,派生类就看不见了,也就不能调用了,此时派生类就无法实例化出对象,就实现了无法继承的功能。而C++11添加了新的关键字final,用final修饰基类,这样这个类就不能被继承了。同时final这个关键字还能用在虚函数上,当基类中的某个虚函数我们不想让派生类重写它时,我们也可以用final修饰。
C++11中还提供了override关键字,被override修饰虚函数一定要重写这个类,这个关键字可以有效放置我们在实现虚函数的重写的时候,函数名写错的问题。
四、lambda
1、lambda的语法
lambda的本质是一个匿名的函数对象,和普通的函数不同的是,它可以在定义在函数的内部。在语法上,lambda没有类型,所以一般我们用auto或者模版参数定义的对象去接受lambda对象。
lambda的格式为:[capture-list] (parameters)-> return type {function body}
[capture-list]是捕捉列表,捕捉列表一定在lambda的开头位置,编译器根据[]来识别接下来的代码是否为lambda,捕捉列表可以用来捕捉上下文中的变量供lambda函数内部使用,捕捉列表可以传值和传引用捕捉。捕捉列表可以为空,但是[]不能省略。
(parameters)是lambda的参数列表,和普通函数的参数列表功能类似,如果不需要传递参数,那么可以连通()一起省略。
-> return type是lambda的返回值类型,当函数没有返回值的时候可以直接省略。当返回值的类型明确时,也可以省略,由编译器对返回类型进行推导。
{function body}lambda的函数体,函数体内的实现和普通函数的函数体实现完全类似,在函数体里除了可以使用lambda内部的参数以为,还能使用捕捉列表捕捉到的变量,同时,若lambda的函数体为空{}也不能够省略。
2、捕捉列表
lambda表达式中默认只能使用lambda函数体和参数列表里的变量,如果想要使用外层作用域中的变量就需要进行捕捉。第一种捕捉方式就是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉多个变量用逗号分割。[x,y,&z],其中x和y是传值捕捉,z是传引用捕捉。在lambda内传值捕捉的变
量不能进行修改,传引用捕捉的变量可以修改。
#include<iostream>
using namespace std;
int main()
{
int a = 1, b = 2;
auto func1 = [a, &b]
{
//a++; 会编译报错
int c = a + b;
b=10;
cout << b << endl;
};
func1();
cout << b;
return 0;
}
第二种捕捉方式就是隐式捕捉,在列表中写一个=表示隐式值捕捉,在捕捉列表中写一个&表示隐式引用捕捉,这样我们在lambda中使用了哪些变量,编译器就会自动捕捉这些变量。
第三种捕捉方式是使用混合使用显示捕捉和隐式捕捉。[=,,&x],表示x引用捕捉,其他变量都是隐式值捕捉,[&,x,y]表示x和y是值捕捉,其他变量都是引用捕捉。在使用混合捕捉时,第一个元素必须是=或者&,并且如果一个元素是&,那么后面的捕捉变量就只能是值捕捉,如果第一个元素是=时,后面的捕捉变量也就一定只能是引用捕捉。
lambda表达式如果在函数的局部域中,他可以捕捉lambda位置之前定义的变量,不能捕捉静态局部变量和全局变量,同时静态局部变量和全局变量也不需要捕捉,可以直接在lambda表达式中直接使用。这就意味着lambda表达式如果定义在全局位置,捕捉列表必须为空。
默认情况下lambda的捕捉列表是被const修饰的,也就是说传值捕捉过来的对象是不能修改的,但是可以在参数列表的后面加上mutable可以取消其常属性,也就是说,使用该修饰符以后,传值捕捉的对象就可以修改了,但是修改的还是形参对象,并不会影响实参,并且在使用该修饰符后,即使没有参数,参数列表也不能省略。
3、lambda的应用
在有lambda之前,我们使用的可调用对象只有函数指针和仿函数对象,函数指针使用起来非常不方便,在一些场景中,一些如果使用仿函数可能会造成不方便,比如说我们在某个地方使用了一个仿函数对象,但是这个仿函数的定义并不在当前文件中,我们就需要花时间去找这个仿函数的功能到底是什么。但是在有了lambda以后,我们在这些需要传仿函数对象的地方就可以直接传入一个lambda,lambda的功能就写在原地,这样就很方便。同时还在智能指针中定制删除器等有很多用途。
4、lambda的原理
lambda的原理和范围for很像,编译后从汇编指令层的角度去看,根本就没有lambda和范围for这样的东西。范围for的底层是迭代器,而lambda的底层是仿函数对象,也就是说我们写了一个lambda以后,编译器会生成一个对应的仿函数的类。
仿函数的类名是编译器按照一定的规则生成的,保证了不同的lambda生成的类名不同,lambda的参数、返回类型、函数体就是仿函数operator()的参数、返回类型和函数体,lambda的捕捉列表的本质是生成仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参。
原文地址:https://blog.csdn.net/Wangx_wang/article/details/143561423
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!