1. C++11简介

C++11,是从C++98之后,一个非常重要的版本,在这个版本中,引入很多非常重要的更新:初始化列表,右值语义下的移动构造和移动赋值,emplace_back和lambda表达式等。

2. 初始化列表

这个初始化列表,不同类构造函数中的初始化列表,而是C++11引入的一种新数据结构:initializer_list

initializer_list这种模板容器类型,构造和赋值都需使用{}这个列表结构进行,只能存储相同类型的数据,与顺序表类似。

2.1 {}

C++11中,引入初始化列表后,提出一个理念:万物皆可用{}进行初始化,即任何类型的数据定义时,用{}初始化,且可以省略赋值操作符。

{}用这个初始化,有两种情况:

  • 一种是隐式类型转换成相应类型,然后再初始化。
  • 另一种是,隐式类型转换成初始化列表类型,然后再用初始化列表进行初始化。当然,必须满足{}为相同类型才能转化为initializer_list,同时相应类型为自定义类型,并且提供了匹配initializer_list的接口:如果以上条件均满足,这是会优先匹配的。

3. 右值引用的移动语义

3.1 C++中的值概念

C++中将值分为两大类:泛左值和右值。

泛左值:可以进行用户层面ODR-USED的值,简单来说,就是用户可以直接取地址的值,我们都称之为泛左值。泛左值又分为:左值和将亡值。左值,主要强调生命周期存有一段时间;将亡值,则指生命周期即将结束。

右值:右值分为将亡值和纯右值。将亡值即泛左值中,纯右值则不能ODR_USED,用户层面无法取地址,通常是一些字面量或者临时对象和匿名对象。
实际上,C++中的右值概念是泛化的,传统意义上的右值应为纯右值,但是C++中,右值泛指那些字面量,或者非字面量中生命周期短暂,资源需要被转移的值。

3.2 左值引用与右值引用

&& 是右值引用,分为const和非const 版本。
& 是左值引用,同样分为const和非const版本。

需要特别注意的是,如果创建一个实际右值引用变量,那么这个变量本身具有左值属性,因为可被ODR_USED。

如果想要让一个变量具有右值属性,可以使用库函数move,本质就是实现一个类型强转功能。

任何值皆可被const&进行引用,但是右值引用只能引用严格具有右值属性的值,即纯右值,绝不能引用左值。

3.3 移动语义

什么是移动语义?移动语义是针对右值提出的。因为右值,基本为生命周期即将结束的值,而且临时对象或匿名对象,往往是一个表达式中的中间值,并不是最终结果——对于自定义类型的右值而言,尤其包含资源时,资源的深拷贝消耗很大,效率不高,而这些右值,申请了资源,却又要很快释放资源,没有意义。因此,就可以把这些右值的资源,转移给相应的左值,进而减少资源的申请和释放,这就是右值引用的移动语义。

3.4 移动语义下的构造,赋值和插入

在什么时候会出现申请资源的深拷贝呢?自定义类型的构造和赋值时。

因此,除却原有的拷贝构造和拷贝赋值引入移动构造和移动赋值。

移动构造和移动赋值是用来进行资源转移的。一个右值对象,会优先匹配移动构造和移动赋值,其次再匹配拷贝构造和拷贝赋值。

移动构造与移动赋值,建议声明为noexcept,即不允许抛异常,因为资源交换的过程不允许被中断,否则可能产生一些问题;而拷贝构造和拷贝赋值,对异常的容忍度高,也实际存在需要catch异常的场景,因为存在new申请资源。

c++11引入移动语义后,相关插入接口基本重载了右值引用的移动版本,如:push_back和insert系列接口。

4. emplace_back

4.1 emplace_back 和 push_back

既然push_back已经重载右值版本,整体效率大大提升,为什么要引入emplace_back?

emplace_back实际是插入效率的进一步提升,在push_back需要构造+移动构造的场景下(即涉及隐式类型转换),emplace_back可以实现直接构造。

那么,emplace_back的直接构造如何实现?其中涉及引用折叠,完美转发以及可变参数模板等知识。

4.2 引用折叠

引用折叠的出现,主要是在万能引用的背景下提出的。

C++中,希望存在万能引用,即传入一个左值,通过左值引用;传入一个右值,通过右值引用——为了实现万能引用,引用折叠规则因此而生。

using type1 = int&&;
using type2 = int&;
template<class T>
void f(T&)
{}

template<class T>
void f(T&&)

引用折叠出现在类型的重命名和模板中,不能自身写多个&,编译器无法解析。

对于左值引用类型,即无论&左侧无论是&,还是&&都最终解析成左值引用,所以最终传入右值或左值,均被左值引用。

对于右值引用类型,&&的左侧如果是&,解析成左值引用;如果是&&,则解析成右值引用。所以,就有万能引用:传入左值,则会自动推导出&,形成左值引用;传入右值,则自动推导出相应类型,构成右值引用(不推导出&&,虽然最终也构成右值引用)

4.3 完美转发

万能引用很方便,但是又引入新问题:因为右值引用的对象本身具有左值属性,万能引用让左值被左值引用,右值被右值引用,但如果调用函数,只能匹配到左值引用的版本。
而实际上,传入右值,是希望资源转移,此时却无法实现。不能直接move,因为左值不允许资源转移。

所以,我们希望传入后,能够保持之前传入值的类型,这样可以实现自行匹配:资源转移或不转移。因此,引入完美转发。

std::forward<T>();

forward本质是一个函数模板,实际使用时,将万能引用模板中的T类型进行函数实例化,然后将相应引用对象作为参数传入,本质也是类型强转——这样即可实现值类型保持。

顺带提一嘴,decltype关键字,decltype,给其对象,会返回对象的实际类型。

4.4 可变参数模板

C中支持可变参数,C++中同样引入了一套可变参数机制:即可变参数模板。

template<class ...Args>
void func(Args... args)
{}

上图中,func函数支持多参数传入,这些参数最终会被打包成一个参数包,参数包类型是Args,采用模板类型(因为不同的参数,打包后,决定参数包类型的不同),参数包对象是args:理解args,可以直接把其拆成多个参数分列理解,实际上起到的作用也是如此。

4.4.1 包扩展

如何对参数包进行解包,即分别拿到参数包中的每一个参数?

有两种常用的解包方式:

void ShowList()
{
	std::cout << “无参数” << std::endl;
}

template<class T,class ...Args>
void ShowList(T x,Args... args)
{
	std::cout << x << std::endl;
	ShowList(args);//本质是在编译时递归解包,通过函数模板的自动推导和函数重载实现
}

template<class ...Args>
void func(Args... args)
{
	ShowList(args);
}
template<class T>
const T& GetArgs(const T& x)
{
	std::cout << x << std::endl;
}
template<class ...Args>
void Argument(Args... args)
{
}
template<class ...Args>
void func(Args... args)
{
	Argument(GetArgs(args)...);
	//此处的含义是将args参数包中的每一个参数分别单独传给GetArgs并调用该函数,如果没有参,则就不再调用GetArgs,最终GetArgs返回的所有参数,再一起传给Argument.
}

除此之外,再介绍一个专门用于计算参数包中参数个数的操作符:sizeof…(args),给相应的参数包对象,可以得到其中参数个数。

4.5 emplace_back的实现

在这里插入图片描述
emplace_back的实现中:

  1. 借助可变参数模板,实现直接传入可变的参数,这些参数是可以用来直接构造自定义类型的对象。特别说明,push_back支持传入{},存在隐式类型转换;但由于emplace_back实现为可变参数模板,反倒不再支持传入{}。
  2. 借助万能引用和完美转发,使得传入的右值对象保持右值,传入的左值保持左值,进而保证能正确匹配相应构造函数。
  3. 为什么是直接构造?前面说过,args可以将其展开成多个参数看待,因此调用构造函数时,可以直接传入——构造函数中,如果实现可变参数模板的,则会优先进行函数包匹配,否则则将函数包展开匹配相应的构造。 对于底层的空间,如果未开辟,则直接new调用相关构造,否则使用定位new,同样是直接构造。

5. lambda表达式

lambda表达式是C++从别的语言中引入的,一种轻量化实现的可调用对象

lambda表达式统一结构:

[capture](parameters) mutable -> return_type{ body };

lambda表达式示例:

int x = 5;
int y = 6;
auto f = [x,&y](int z) mutable -> void{
	std::cout << x << std::endl;
	std::cout << y << std::endl;
	std::cout << z << std::endl;
};

5.1 lambda表达式结构

lambda表达式结构与函数类似,有函数体,函数参数等。
但是lambda表达式不用写出函数名,同时存在捕捉列表[ ]结构。
lambda表达式的返回值可以显式通过 -> 指明,也可以依赖编译器自动推导。

因为lambda表达式希望能够在函数体中使用一些对象,同时又不希望通过参数形式传入,因此引入捕捉列表。

捕捉列表即对lambda表达式外部变量进行捕捉,分为值捕捉和引用捕捉。

  • 值捕捉。值捕捉直接写对象名,捕捉到的对象本质是外部对象的拷贝,具有const属性,如果要修改,需要在lambda表达式传入参数表,即()后,添加mutable.
  • 引用捕捉。引用捕捉使用&,捕捉到的对象是外部对象的引用,不具有const属性,可直接修改,引用会影响外部对象。

当然,也可以值捕捉和引用捕捉混用。除此之外,还有隐式捕捉和显式捕捉之分:显式,就是明确显出捕捉哪个对象;隐式,就是只表明是值捕捉 = 还是引用捕捉 & ,根据函数体中实际使用到的对象,编译器自行进行捕捉。

5.2 lambda表达式原理

每一个lambda表达式都具有自己的唯一类型,这个类型是编译器生成的,我们上层一般无法直接得知。因此,lambda表达式对象,类型一般都写成auto,类型由编译器自行推导。

在C++中,lambda表达式,底层本质就是一个仿函数,实现了对operator()的重载。

而捕捉列表中捕捉的对象,本质就是类中成员变量:非mutable值捕捉,就是类中定义相应const 对象,并在构造中完成拷贝,加上mutable,就是去除const关键字;引用捕捉,就是类中定义相应引用对象,并在构造中完成引用。

6. 包装器

6.1 function

在C++中,一般存在三种可调用对象:函数,仿函数和lambda。

为了统一可调用对象,实现泛型编程,C++提出通过包装器对可调用对象进行包装,进而通过包装器实现相关可调用对象的调用。

std::function<ret(parameters)>

function本质是一个类模板,一般采用上述格式进行实例化,在<>中标明可调用对象的返回值类型,以及需要传入的参数。上述实例化写法比较特殊,编译器是会特殊处理的。

来看一些function包装可调用对象的示例:

void Print()
{
	std::cout << "hello" << std::endl;
}

int add(int x,int y)
{
	return x + y;
}

class Cmp
{
	public:
		bool operator()(int x,int y)
		{
			return x < y;
		}
}

int main()
{
	std::function<void()> f1 = Print;
	std::function<int(int,int)> f2 = add;
	std::function<bool(int,int)> f3 = Cmp();
	std::function<void()> f4 = [](){std::cout << "hello" << std::endl;};
	f1();
	(void)f2(1,2);
	(void)f3(1,2);
	f4();
	return 0;
}

如果function包装类中非静态成员函数,又有所不同:类中非静态成员函数存在一个this指针的隐式参数,需要依赖具体对象,所以function实际实例化和使用时,需要额外传入参数。

class A
{
public:
	int add(int x, int y)
	{
		return x + y;
	}
};

int main()
{
	A a;
	std::function<int(A, int, int)> f1 = &A::add;
	std::function<int(A*, int, int)> f2 = &A::add;
	std::function<int(A&, int, int)> f3 = &A::add;
	std::function<int(A&&, int, int)> f4 = &A::add;
	std::cout << f1(a, 1, 2);
	std::cout << f2(&a, 1, 2);
	std::cout << f3(a, 1, 2);
	std::cout << f4(std::move(a), 1, 2);
	return 0;
}

上述代码中,拿出类中成员函数的地址必须添加&。实际实例化过程中,可以用类的类型,指针,左值引用,和右值引用均可,然后使用时传入相应对象,最终底层调用,编译器都会处理成传入一个相应的隐式指针。

function本质是一个类模板,底层本质也是借助仿函数的原理实现的——函数便包装函数指针,仿函数便包装仿函数对象,lambda表达式便包装lambda表达式对象。

6.2 bind

上述function实现包装的过程中,对于类中成员函数的包装,略显冗余:类对象这个参数总是需要固定传入的。

针对这个问题,bind包装器,不仅仅可以用来包装可调用对象,还可以控制可调用对象的传参形式。

bind本质是一个模板函数,包装成功后,会返回一个对象,本质也是仿函数,之后便可通过该对象进行调用。

bind实现传入参数的控制,是通过placeholders实现的。

placeholders是C++库中实现的一个类,其中定义了如_1,_2,_3这样的占位符,用来指代实际函数调用过程中传入的参数:_1就表示参数列表中从左往右的一个参数,其它以此类推。

那么bind是如何实现参数控制的呢?

再以上面的示例为例:

using std::placeholders::_1;
using std::placeholders::_2;
class A
{
public:
	int add(int x, int y)
	{
		return x + y;
	}
	int sub(int x,int y)
	{
		return x - y;
	}
};

int main
{
	A a;
	auto f1 = std::bind(&A::sub,a,_1,_2);
	auto f2 = std::bind(&A::sub,a,_2,_1);
	std::cout << f1(1,2) << std::endl;//输出-1:sub(1,2)
	std::cout << f2(1,2) << std::endl;//输出1:sub(2,1) 注意差别
	return 0;
}

上述绑定何意?bind绑定A类中的成员函数add。add函数调用依赖于三个参数,具体对象,以及x,y。具体对象是固定的,为a,所以bind中就指明a,这样之后每次调用时,底层总是默认传入a。_1和_2均为占位符,分别对应实际f1调用过程中传入的两个参数,_1和_2是不确定的,由外界传入参数决定。

在上述bind中,bind绑定到特定可调用对象后,之后的参数实际决定了底层调用时传入参数是什么,以及传入参数的顺序:是什么可以固定,也可由外界决定(通过占位符实现);参数顺序也可更改,如上述代码中的f2,但必须与底层可调用对象的参数列表类型顺序照应。

更多推荐