强力的C++11 引用、移动、右值、折叠、完美转发、lambda

目录

C++11中的初始化列表

右值引用与移动语义

左值与右值

左值引用右值引用

引用延长生命周期

※左值和右值的参数匹配

右值引用与移动语义

移动构造与移动赋值

移动构造

noxecept在移动构造的使用

移动赋值

右值引用和移动语义解决传值返回的拷贝问题

拷贝构造编译器的相关优化原理:

怎么理解第二步仅进行的三次构造?

 拷贝赋值相关优化:

右值引用和移动语义在传参中的提效

类型分类

引用折叠

完美转发(perfect forwarding)

可变参数模板

emplace系列接口

lambda表达式

lambda原理

语法结构

捕捉列表

使用示例:

使用lambda与sort函数按照货物不同的属性来排序

包装器

function

认识:

bind

新增的类功能11/98

默认的移动构造与移动赋值

default与delete

final与override

STL于11/98(ITVW)


4-24 + 4-25完成

C++11中的初始化列表

C++后试图实现一切对象都可以使用{}初始化,{}初始化操作也叫做列表初始化。

{}初始化时可以省略掉"="。

C++11库内提出初始化列表的类:std::initializer_list,auto il = { 10, 20 ,30 };      //il的类型是一个初始化列表,其底层是开一个数组再使用迭代器将数据拷贝过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。

像vector中的initializer_list:

右值引用与移动语义

左值与右值

左值是一个表达式代表给一个数据起了名字,我们可以对这个左值取地址来获得此数据的存储位置。 像: a   s[0]   *p 等等

右值直接指向了数据本身,因此就不能取地址(不会出现:&3),一般存储在内存中的数据段(只读变量 可执行代码)。像:临时对象 x + y 匿名对象string("abc")  传值返回add(x, y)

左值和右值的重要区别在于能否进行取地址操作

左值引用右值引用

※返回右值引用的函数:
move(x)                 //标准库函数:将传入的参数x无条件转换为右值引用,无论x是左值还是右值。
static_cast<X&&>(x) //它将 x 强制转换为 X&& 类型,表明x可以被移动。

std::vector<int> v1{1, 2, 3};

// 这两种写法等价
std::vector<int> v2 = std::move(v1);           //直观
std::vector<int> v3 = static_cast<std::vector<int>&&>(v1);//等效不直观

Type1& l = a;            //左值引用  对左值取别名
Type2&& r = pvalue;//右值引用   对右值起别名

左值引用不能直接引用右值,使用const辅助才可以。 
右值引用不能直接引用左值,但是右值引用可以引用move(左值)

move的原型底层是强制类型转换 还有一些引用折叠的知识

template<typename T>
typename remove_reference<T>::type&& move(T&& arg);
                                     涉及引用折叠的万能模板

右值引用虽然引用的右值但规定右值引用也是左值属性。

引用延长生命周期

本质是对一些临时对象进行的生命周期的延长。

※左值和右值的参数匹配

与函数重载类似函数调用总是寻找最匹配的形参类型函数进行调用。

右值引用与移动语义

移动构造与移动赋值

移动构造

移动构造(move construction)是类的构造函数的一种与拷贝构造函数一样是构造函数的重载,不过移动构造函数要求第一个参数必须为右值引用类型如果有其他参数必须缺省

谁才有移动构造的方式?

深拷贝的类型?

移动构造的本质是指针底层的改变、赋值: 
※//err误以为是浅拷贝导致的数据移动

class String
{
public:
	String(String&& OthrS) noexcept  
		//资源掠夺
		:_data(OthrS._data)
		, _size(OthrS._size)
	{
		//资源置空!!!
		OthrS._data = nullptr;
		OthrS._size = 0;
	}
private:
	char* _data;
	size_t _size;
};
noxecept在移动构造的使用

noexcept表示该函数不会抛异常便于编译器优化。 如果不加:

移动赋值

移动赋值(move assignment)是拷贝赋值函数的重载函数,移动赋值要求第一个参数是右值引用的类型。类似的它也是允许对象在赋值时"窃取"另一个即将销毁的对象的资源,避免了不必要的拷贝开销。

class ResourceHolder
{
public:
	ResourceHolder& operator=(ResourceHolder&& rh)
	{
		//第一步肯定是释放原资源否则泄漏
		delete[] data;
		//掠夺
		data = rh.data;
		size = rh.size;

		rh.data = nullptr;
		rh.size = 0;
	}
private:
	int* data;
	size_t size;
};

右值引用和移动语义解决传值返回的拷贝问题

拷贝构造编译器的相关优化原理:

怎么理解第二步仅进行的三次构造?

nums1、nums2的构造  +  str的构造(直接基于ret指向的空间进行构造类似于"别名")。

 拷贝赋值相关优化:

右值引用和移动语义在传参中的提效

C++11后的容器内push和insert系列的接口增加了右值引用的版本。

当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象。

当实参是一个右值时,容器内部则调用移动构造,右值对象的资源(进行掠夺 原右值变得无资源)到容器空间的对象上。

类型分类

纯右值(pure val):字面值常量或求值结果相当于字面值或是一个不具名的临时对象。如:42 a + b
将亡值(expiring val):指返回右值引用转换为右值引用的函数的调用表达。如:

move(x)、static_cast<X&&>(x)

引用折叠

多引用造成的效果是单引用。

规定不能直接定义引用的引用如:int& && r = i;但是可以通过模板或typedef来构成引用的引用。 因此诞生了引用折叠的概念。

右值引用的右值引用是右值引用,左值引用的左值引用是左值引用。
右值引用的左值引用是左值引用,左值引用的右值引用是左值引用。

规定:对于拥有右值引用作函参的函数f2(T&& t);假设T是int右值,T推导为int实参是左值,模板参数T推导为int&,结合引用折叠规则就出现了实参是左值推导类型为左值引用,实参是右值推导类型为右值引用的情况。所以称这样的以右值引用为函数参数类型的模板函数也叫做万能模板(这是以左值引用为模板参数做不到的)。

实例1:

template<typename T>
void f2(T&& x)
{}
using namespace std;
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<typename T>
void f1(T& t)
{ }

//右值引用作模板参数的"万能模板"
template<class T>
void f2(T&& t)
{ }

int main()//注:注释部分为不符合正确语法的部分
{
	typedef int& lref;
	typedef int&& rref;
	int n = 0;
	lref& r1 = n; // r1 的类型是 int&
	lref&& r2 = n; // r2 的类型是 int&
	rref& r3 = n; // r3 的类型是 int&
	rref&& r4 = 1; // r4 的类型是 int&&
	//右值引用+右值引用

	// 没有折叠->实例化为void f1(int& x)
	f1<int>(n);
	//f1<int>(0); //左值引用传右值

	// 折叠->实例化为void f1(int& x)
	f1<int&>(n);
	//f1<int&>(0);//左值引用+右值引用传左值

	// 折叠->实例化为void f1(int& x)
	f1<int&&>(n);
	//f1<int&&>(0);//右值引用+左值引用传左值

	// 折叠->实例化为void f1(const int& x)
	f1<const int&>(n);
	f1<const int&>(0);

	// 折叠->实例化为void f1(const int& x)
	f1<const int&&>(n);//const 左值引用
	f1<const int&&>(0);//const 左值引用

	//右值引用函参

	// 没有折叠->实例化为void f2(int&& x)
	//f2<int>(n);//右值引用传右值
	f2<int>(0);

	// 折叠->实例化为void f2(int& x)
	f2<int&>(n);
	//f2<int&>(0);//左值引用传左值

	// 折叠->实例化为void f2(int&& x)
	//f2<int&&>(n); //右值引用传右值
	f2<int&&>(0);

	return 0;
}

实例2:

 万能引用

完美转发(perfect forwarding)

格式:

template<typename T>
void wrapper(T&& arg)
{
	//保留原始属性
	EFction(std::forward<T>(arg));
	return;
}

当涉及引用的引用时,若引用原本是右值引用 当我们再次传递时传递的是其具备的"左值属性",因此会匹配到与预期不符的对象上。“完美转发”正是解决这种再次传递时属性改变问题:

实例:

STL环境中的例子

 push_back下Insert为例:完美转发、右值引用的万能模板使用

右值引用的不不规范操作:

操作左值move掠夺了左值资源资源—可能会导致原资源为空

可变参数模板

引入此语法的起点:实现 参数个数 类型 的灵活自动推导和简单任务(单一任务)的实现。 可进阶理解为二阶模板。

template<class ...Args> void Func(Args... args);
template<class ...Args> void Func(Args&... args);
template<class ...Args> void Func(Args&&... args);

1.使用"..."来表示这是函数参数或函数参数的包类型。
2.本质是进行多个重载函数的依次展开
3.sizeof...(args);  //规定此为计算参数个数的新格式。     参数包使用时很多代码格式莫名其妙的,必要时必须查文档了。
参数模板的应用与递归 切割 体现。 //也有折叠的展开方式C++17 此处不作介绍

//注释此处分析
//void ShowList()
//{
//	// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数
//	cout << endl;
//}

template <class T, class ...Args>
void ShowList(T x, Args... args)
{
    //此处的递归终结语句并没有起到作用
	if (sizeof...(args) == 0) return;
	cout << x << " ";
	// args是N个参数的参数包
	// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包
	ShowList(args...);
}

// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
	ShowList(args...);
}

为什么递归终结语句没有起到作用?

体现"编译期"与"运行期"的矛盾:
编译时:
ShowList结合参数包推导并进行语法检查,
发现:if (sizeof...(args) == 0) return;没有语法语义错误
但是 没有找到showlist的重载函数因此报错(因为模板(是T的原因不是参数包的原因)不是真正的函数)。
解决方法:随便写一个ShowList的重载函数即可。

运行时:
参数包纳入传递过去的参数集(Print函数可加可不加提高结构性、可读性)。
执行代码逻辑。

emplace系列接口

emplace区别于一般的插入它可以实现直接在容器内部构造元素后插入而非先构造后插入。

template<class ...Args> void emplace_back(Args&& ...args);
template<class ...Args> iterator emplace(const_iterator position, Args&&... args);

emplace是C++11下新增的系列接口,功能上兼容push与insert等接口。  emplace与insert和push等功能上各有千秋但emplace总体上优于其他接口。

完美转发的格式:std::forward<Args>(args)...

在VS2022下empace_back与push_back原型的对比:

push_back较于emplace_back增加了const val_type& val的接口出现了以下功能差异情况:

1.临时对象下的区别

2.键值对下的区别

lambda表达式

lambda原理

lambda的底层是仿函数对象,编译器实际生成的是一个仿函数的类。

仿函数的类名是按照一定的规则生成的,保证了每个lambda表达式生成的类名不同。labmda的返回类型、参数和表达式就对应仿函数的返回类型、参数和表达式。

语法结构

[捕捉列表](参数列表) ->返回类型{函数体};

参数列表及其括号可省略,"->返回类型"也可以省略,交由编译器推导。 //两端不可省略中间中间两者均可以省略。

捕捉列表:总是出现在labmda函数的开头位置,编译器根据[]来判断接下来的代码释放为lambda函数,捕捉列表能够捕捉上文的变量供lambda函数函数使用,捕捉列表捕捉方式拥有多种模式:

捕捉列表

显示指定捕捉在捕捉列表中显示传递,捕捉的多个变量用逗号分割。[x, y, &z]表示x、y的const值捕捉以及z的引用捕捉。

隐式捕捉,在列表中写一个=表示隐式捕捉、&表示隐式引用捕捉。

混合使用显示与隐式捕捉,[=,&a]表示其他变量隐式值捕捉单独a使用隐式引用捕捉,[&,a,b]表示其他变量使用隐式引用捕捉a,b使用值捕捉。当使用混合捕捉的方式时第一个必须是&或=,所以若第一个是&那么其他的必定是值捕捉,若第一个是=那么其他的必须是引用捕捉。

默认情况下,值捕捉下的变量是const+拷贝值无法修改,若要求修改则需要加mutable(易变的)在()后加—必须有()。

auto的使用原因:

接收类(仿函数的类)名专由的编译器决定的不确定性,uuid(百度百科)码。

使用示例:

使用lambda与sort函数按照货物不同的属性来排序
struct Goods
{
	string _name; // 名字
	double _price; // 价格
	int _evaluate; // 评价
	// ...
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{
	}
};

struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};
int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3
	}, { "菠萝", 1.5, 4 } };
	// 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中
	// 不同项的⽐较,相对还是⽐较⿇烦的,那么这⾥lambda就很好⽤了
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) 
	{
		return g1._price < g2._price;
	});
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) 
	{
		return g1._price > g2._price;
	});
}

包装器

function

定义在funtional的头文件中。

std::function是一个包装器std::function的实例对象可以包装此处其他可调用对象,由如:lambda\bind\仿函数等储存的可调用对象被称为std::function的目标。若std::function不含目标则称其为空,调用其目标时抛出异常std::bad_functional_call的异常。

认识:

由其定义可以看出包装器function的重大作用就是"包装"的效果,包装下使得一切包括lambda函数、bind表达式等具备一统一的调用调用方式此时我们使用时就可以灵活地调用与高效地维护代码逻辑。

实例1:

#include<functional>
int f(int a, int b)
{
	return a + b;
}
struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}
};

int main()
{
	// 包装各种可调⽤对象
	//      第一个参数类型
	function<int(int, int)> f1 = f;
	// 返回类型 第二个参数类型
	function<int(int, int)> f2 = Functor();
	function<int(int, int)> f3 = [](int a, int b) {return a + b; };

	cout << f1(1, 1) << endl;
	cout << f2(1, 1) << endl;
	cout << f3(1, 1) << endl;
}

实例2:(映射的具体应用)

bind

同样地bind也被包括于funtional的头文件中。

bind也是一个可调用对象的包装器,可以把它看作一个函数适配器,对于接受的fn可调用对象进行处理后,返回一个可调用对象。

bind的主要功能:调整调用时参数的个数(适时规定死参)与调整参数的顺序。

结构式:

auto newCallable = bind(callable, arg_list);其中newCallable,本身是一个可调用对象,arg_list是一个逗号分割的参数列表,对于给定的callable的参数。当我们调用newCallables时,neweCallable会调用callable,并传给arg_list的参数中去。

常定义typedef以:

using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

结合使用,这些"_1"、"_2"、"_3"作为bind内的占位符,_n中的n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数_2为newCallable的第而个参数……

实例:

#include<functional>
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;

int Sub(int a, int b)
{
	return (a - b) * 10;
}

int main()
{
	auto sub1 = bind(Sub, _1, _2);
	cout << sub1(10, 5) << endl;//10代表第一个实参_1占位了
								//5代表第二个实参_2占位了

	auto sub2 = bind(Sub, _1, 99);
	cout << sub2(10) << endl;
	//此处即使写入第二个参数依旧使用callable的参数(99)

	auto sub3 = bind(Sub, _2, 99);
	cout << sub3(1,10) << endl;//未使用第一个参数(不写第一个参数报错)

	return 0;
}

新增的类功能11/98

以下内容均涉及架构关系的考量在一般练习中不会涉及,但若在实际工作中,是必备的基础知识要加以适当地理解与运用。

默认的移动构造与移动赋值

移动构造函数与移动赋值函数作为新增的默认成员函数,在11中新增。
移动构造作为构造函数的重载,移动赋值作为赋值运算符的重载函数存在。

注意:

1.在新增的构造函数中要额外注意二者关于对应函数的重载矛盾,千万不要认为二者作为类内的独立存在。

2.在"移动"中是掠夺的本质,但根据官方文件1(中文版文档)官方文件2(英文文档),规定"可能造成原资源的置空",这导致是否置空取决于编译器的决定。因此我们显示实现时永远手动将原资源释放、置空是必备的好习惯。

default与delete

前瞻:指定需要编译器支持此类函数的生成default。
           禁止外部调用限制delete。

default重点记忆。

class widget
{
    //指定需要编译器自动生成的默认构造函数
    widget() = default 
    { }
    
    //指定需要编译器自动生成的拷贝构造函数的支持
    widget(const widget& wdt) = default 
    { }

    //指定需要编译器自动生成的析构函数
    ~widget() = default
    { }
}

delete

  • 禁止拷贝语义(如单例类、资源句柄类)
  • 禁用特定参数类型的重载函数
  • 防止隐式类型转换导致的意外调用
class UinqueResource
{
public:
    UniqueResource() = default;//我需要自动生成的构造函数
    UniqueResource(const UniqueResource&) = delete;//禁止拷贝函数调用
                                        //这是语句所以要加;
}

final与override

final表示此类内虚函数/类无法被重载/继承。

STL于11/98(ITVW)

unordered_map、unordered_set的新增。
右值引用与移动语句系列的:
push、insert、emplace的扩展。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值