1.完美转发

1.1 模板中的&& ---万能引用

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值,模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力。

如下面的代码,T&&在这里就代表了万能引用,既可以接收左值,又可以接收右值。

template<typename T>
void PerfectForward(T&& t)
{
 Fun(t);
}

我们看以下代码:

PeffectForward分别接收了左值,右值,const左值,const右值,由它调用的func函数应当调用相对的func函树。

void Fun(int& x) { cout << "左值引用" << endl; }

void Fun(const int& x) { cout << "const 左值引用" << endl; }

void Fun(int&& x) { cout << "右值引用" << endl; }

void Fun(const int&& x) { cout << "const 右值引用" << endl; }

template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}

int main()
{
	PerfectForward(10);  //右值
	int a;
	PerfectForward(a);//左值

	PerfectForward(std::move(a));//右值

	const int b = 8;
	PerfectForward(b); //const 左值

	PerfectForward(std::move(b)); // const右值
	return 0;
}

但是调用结果却和我们想象的不一样,这就是我们万能模板的存在的缺点,当他引用右值的时候,会退化成左值,知道这点之后这样的结果就说的通了。这时,我们就需要这里讲到的完美发forward关键字,他可以保留对象原生类型属性。

这样修改后代码的结果便和我们的预期相符合了

template<typename T>
void PerfectForward(T&& t)
{
	
	Fun( forward<T>(t));
}

2.新的类功能

默认成员函数

原来C++类中,有6个默认成员函数:

1. 构造函数

2. 析构函数

3. 拷贝构造函数

4. 拷贝赋值重载

5. 取地址重载

6. const 取地址重载

最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。

C++11 新增了两个:移动构造函数和移动赋值运算符重载

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

1.如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类 型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

2.如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内 置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)

3.如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

强制生成默认函数的关键字default:

C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原

因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以

使用default关键字显示指定移动构造生成。

class Person
{
    public:
     Person(const char* name = "", int age = 0)
     :_name(name)
     , _age(age)
     {}

     Person(const Person& p)
     :_name(p._name)
     ,_age(p._age)
     {}

    Person(Person&& p) = default;

    private:
     bit::string _name;
     int _age;

    };

    int main()
    {

     Person s1;
     Person s2 = s1;
     Person s3 = std::move(s1);
     return 0;
    }

禁止生成默认函数的关键字delete:

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁

已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即

可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数

class Person
{

public:
 Person(const char* name = "", int age = 0)
 :_name(name)
 , _age(age)
 {}

 Person(const Person& p) = delete;

private:

 bit::string _name;
 int _age;

};

int main()
{
 Person s1;
 Person s2 = s1;
 Person s3 = std::move(s1);
 return 0;
}

3.可变参数模板

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比

C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改

进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现

阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了,所以这里我们点到为止,以后大

家如果有需要,再可以深入学习。

最简单的可变参数模板的的函数模板

template<class ... Args> //Args 是一个模板参数包
void Showlist(Args... ars) //args是函数的形参参数包  //这个函数包可以包含0到任意个参数
{

}

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数

包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,

只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特

点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变

参数,所以我们的用一些奇招来一一获取参数包的值

递归函数方式展开参数包

我们通过递归的方式来打印里面的参数,当传入(1,2,3,4,5,6,7,8,9,0)时,会调用第三个Showlist函数,将第一个参数 1 传给了value ,每次调用都会少一个值,直到参数包只有一个参数时,就会调用第一个Showlist函数。这样我们就将参数包里面所有的值都打印出来了。

template<class T>
void Showlist(const T& t)
{
	cout << t << " ";
}

void Showlist()
{
	
}

template<class T , class ... Args> //Args 是一个模板参数包
void Showlist(const T& value , Args... ars) //args是函数的形参参数包  
                                            //这个函数包可以包含0到任意个参数
{
	cout << value << " ";
	Showlist(ars...);
}


int main()
{

	Showlist(1,2,3,4,5,6,7,8,9,0);
	return 0;
}

我们还可以通过一个函数打印除参数包里面的个数:在sizeof后面加上 ... 在加上形参参数包

template<class ... Args>
void Func(Args... args)
{
	cout << sizeof...(args) << endl;
}

逗号表达式展开参数包

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg

不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式

实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式expand函数中的

逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行 printarg(args),再得到逗号表达

式的结果0。同时还用到了C++11的另外一个特性——初始化列 表,通过初始化列表来初始化一个

变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0),(printarg(arg3),0),

etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数

组的过程中会先执行逗号表达式前面的部分printarg(args) 打印出参数,也就是说在构造int数组的

过程中就将参数包展开了,这个数组的目的纯粹是为了在 数组构造的过程展开参数包

template <class T>
void PrintArg(T t)
{
 cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
 int arr[] = { (PrintArg(args), 0)... };
 cout << endl;
}
int main()
{
 ShowList(1);
 ShowList(1, 'A');
 ShowList(1, 'A', std::string("sort"));
 return 0;
}

4.lambda表达式

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

1. lambda表达式各部分说明

[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[ ]来

判断接下来的代码是否为lambda函数捕捉列表能够捕捉上下文中的变量供lambda

函数使用

(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以

连同()一起省略

mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量

性。使用该修饰符时,参数列表不可省略(即使参数为空)。

->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回

值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推

{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获

到的变量。

注意:

在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为

。因此C++11中最简单的lambda函数为:[ ] { }; 该lambda函数不能做任何事情。

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用

[var]:表示值传递方式捕捉变量var

[=]:表示值传递方式捕获所有父作用域中的变量(包括this)

[&var]:表示引用传递捕捉变量var

[&]:表示引用传递捕捉所有父作用域中的变量(包括this)

[this]:表示值传递方式捕捉当前的this指针

int main()
{
	// 最简单的lambda表达式, 该lambda表达式没有任何意义
	[] {};

	// 省略参数列表和返回值类型,返回值类型由编译器推导为int
	int a = 3, b = 4;
	[=] {return a + 3; };

	// 省略了返回值类型,无返回值类型
	auto fun1 = [&](int c) {b = a + c; };
	fun1(10);
		cout << a << " " << b << endl;

	// 各部分都很完善的lambda函数 //这里的&b是&操作符的一个特列,这里表示捕捉所有的值但是 b是引用捕捉
	auto fun2 = [=, &b](int c)->int {return b += a + c; };
	cout << fun2(10) << endl;

	// 复制捕捉x
	int x = 10;
	auto add_x = [x](int a) mutable { x *= 2; return a + x; };
	cout << add_x(10) << endl;
	return 0;
}

意:

a. 父作用域指包含lambda函数的语句块

b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割

比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量

[&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

c. 捕捉列表不允许变量重复传递,否则就会导致编译错误

比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

d. 在块作用域以外的lambda函数捕捉列表必须为空

e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者

非局部变量都会导致编译报错。

f. lambda表达式之间不能相互赋值,即使看起来类型相同.

函数对象与lambda表达式

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的
类对象。

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如
果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐