实用指南:C++11(二)
目录
前言
上篇我们讲完了完美转发,但是C++11要讲的内容很多,所以我们就将剩下的内容放到了这篇来讲,准备接受知识的醍醐灌顶吧孩子们。

一、可变参数模板
1、基本语法及原理
从C++11以后,就出现了可变参数模板,也就是说出现了支持可变参数的函数模板和类模板。可变参数被称为参数包,其实就是把参数打包到了一块取了个名字叫做参数包,参数包分为两种:函数参数包和模板参数包。
template void Func(Args... args) {}
template void Func(Args&... args) {}
template void Func(Args&&... args) {}
我们用省略号来表示一个模板参数或函数参数是一个包,在模板参数列表中,class...或 typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...指出 接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板 一样,每个参数实例化时遵循引用折叠规则。
我们可以使用 sizeof... 来计算参数包中有几个参数。如下:
template
void Print(Args&&... args)
{
cout << sizeof...(args) << endl;
}
2、包扩展
参数包其实只是我们看到的样子,在底层编译时包会扩展成正常的多参数的样子。当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元 素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号(...)来触发扩展操作。
我们来看看底层的原理(包扩展会在编译时扩展完毕):

可变参数模板参数包的处理方式有两种:一种是直接在调用处展开参数包(最常见的方式,一会要讲的emplace接口就是使用的这种方法),第二种是递归分解(需要终止函数,上面图中展示的方式就是递归分解)。
3、emplace 系列接口
C++11以后STL新增了 emplace 系列接口,模板参数均为可变参数。
template void emplace_back (Args&&... args);
template iterator emplace (const_iterator position, Args&&... args);
emplace 系列接口兼容我们平常使用的 push 和 insert 系列函数的功能,并且更改了一下插入的逻辑,大大提高了插入的效率。那具体有什么不同呢?
insert 和 push 系列函数是先在容器外部构造临时对象,然后再将构造好的临时对象移动或拷贝到容器内,比如有以下例子:
vector vec;
vec.push_back("Hello");
//这里就是先用const char*类型的值调用string构造函数构造出一个string类的临时对象
//然后将这个对象通过移动构造或拷贝构造转移到容器vector中去
但是 emplace 系列的插入不会产生临时对象,而是直接在容器内部进行构造,也就是说少了一次移动的过程(如果移动时采用的是移动构造则花销不大,如果是拷贝构造花销不就大了嘛)。没有对比大家可能看不出来区别,我给大家画张图:

所以,emplace 的插入接口确实比 push 和 insert 接口更高效,所以以后我们尽量多使用 emplace 的插入接口。由于 emplace 插入是可变参数,所以他是多参数直接传入即可,不需要使用列表初始化,但是当存储值类似于 pair 类型时,push 插入需要使用列表初始化,以 push_back 和 emplace_back 为例:
list> lt;
lt.emplace_back("苹果", 3);//不需要列表初始化
lt.push_back({ "苹果", 3 });//需要列表初始化
二、新的类功能
1、默认生成的移动构造和移动赋值
原来C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重 载/const 取地址重载,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器 会默认生成。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
如果你没自己实现移动构造函数,且析构函数 、拷贝构造、拷贝赋值重载都没有实现,那么编译器会自动生成一个默认移动构造(一般这四个默认成员函数是绑定的,要写都写,要不写都不写,因为几乎都是需要深拷贝时才需要自己写)。默认生成的移动构造函数,对于内置类型成员会进行浅拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且析构函数 、拷贝构造、拷贝赋值重载都没有实现,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会进行浅拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
2、成员变量在声明时给缺省值
没错,这个玩意也是C++11才出现的,记得我们说过,如果没有在构造函数的初始化列表进行显示初始化,就需要在成员变量声明的地方给上缺省值(这里的初始化列表是我们在类和对象那里讲的一种语法,而不是C++11加入的初始化列表类型)。忘记的去看我的类与对象(下)(一)那篇文章。
3、default 和 delete
C++11 就可以更好地控制我们的默认生成的成员函数了。
default 关键字可以让编译器强制生成一个默认成员函数。比如,由于我们自己写了拷贝构造,系统就不会默认生成移动构造了,但是我们可以使用 default 关键字强制生成该函数。用法就是写一个函数声明,在后面加上 = default,我们以移动构造为例:
class Person
{
Person(const Person& per)//拷贝构造
{
...
}
Person(Person&& per) = default;
....
}
delete 关键字用法和 default 相同,但是作用和 default 作用正好相反,他的作用是限制默认成员函数的默认生成,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。看用法:
Person(const Person& per) = delete;
三、可调用对象
首先声明一下,可调用对象并不是 C++11 才出现的概念,在 C++11 之前就已经出现了。我们先来介绍一下什么是可调用对象。
在 C++ 中,可调用对象并不一定是对象,它是一个比较宽泛的概念,凡是可以用 () 调用的对象在 C++ 中都叫做可调用对象。比如函数指针,函数对象(比如仿函数),普通函数都属于可调用对象。可以看到,这里的函数指针和普通函数并不属于对象,但他们依然属于可调用对象范围内。
所以一般传可调用对象的位置都可以传普通函数,传普通函数的位置也几乎都可以传可调用对象,因为编译器不会管你是不是对象,只会管你是不是可调用的。比如,传仿函数的位置也可以传普通函数(因为普通函数可以隐式转换为仿函数调用的形式)。比如以下例子:
#include
#include
// 普通函数
bool compare(int a, int b) { return a > b; }
// 仿函数
struct Comparator {
bool operator()(int a, int b) const { return a < b; }
};
int main() {
std::vector v = {3, 1, 4, 1, 5};
// 传递普通函数
std::sort(v.begin(), v.end(), compare);
// 传递仿函数
std::sort(v.begin(), v.end(), Comparator());
}
有人会问,既然可调用对象不是 C++11 才出现的,为什么要在这里讲,首先是因为 C++11 新加入了好几个可调用对象,其次是因为我们有时传参会传入可调用对象(主要是为了告诉大家函数也属于可调用对象,需要传可调用对象的地方也可以传普通函数),我们接下来一一讲解新加入的可调用对象。
1、lambda 表达式
是的,你没看错,就是 python 里的 lambda 表达式,听说是抄的,甚至将这个语法挪过来都没咋改,这样就让我们使用这个语法的时候看起来很违和,没错,这个表达式的风格很不C++。
1.1、lambda 表达式的语法
lambda 表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda 表达式在使用层面上没有类型名(具体看原理),所以我们一般用 auto 或模板参数定义的对象(比如仿函数对象,所以仿函数对象可以用lambda表达式代替)来接收 lambda 对象。
来看看 lambda 表达式的格式:[捕捉列表] (参数列表)->返回类型 { 函数体 }
捕捉列表里的内容可以空着,[] 不可以省略
参数列表如果不需要传参可以连着 () 一起省略
->返回类型可以省略不写,因为编译器会自行推倒
函数体可以空着,但是这里的 {} 不可以省略
所以一个最简单的 lambda 表达式是这样的 [ ]{ }
直接来看例子,捕捉列表先空着,我们一会再讲,这里直接空着就行:
auto add1 = [](int x, int y)->int {return x + y; };
//不过一般会省略->返回类型
//auto add1 = [](int x, int y) {return x + y; };
1.2、捕捉列表
lambda 表达式默认只能使用函数体中定义的变量或函数参数中的变量,如果需要使用外层作用域的变量就需要进行捕捉。
这里有三种捕捉方式:
1、在捕捉列表中显式地进行传值捕捉和引用捕捉,捕捉多个变量要用逗号分割,如下:[x, y, &z] 表示的就是捕捉 x 和 y 的值,对 z 引用捕捉。
2、隐式捕捉:在捕捉列表里只写一个 = 就表示隐式传值捕捉(像这样:[ = ]),只写一个 & 就表示隐式引用捕捉(像这样:[ & ])。隐式捕捉后在函数体里默认可以使用所以可以捕捉的参数,但是并不是将这些参数全都捕捉,而是使用哪个捕捉哪个。
3、混合捕捉:就是上面的混起来用,比如 [ = , &x] 就表示除了x是引用捕捉,其他都是传值捕捉。
lambda 捕捉列表可以捕捉当前作用域内在 lambda 表达式之前定义的变量,但是不能捕捉静态局部变量和全局变量(因为这两种变量不需要捕捉就可以直接使用)。
传值捕捉默认不可以在函数体内部修改参数的值,引用捕捉可以修改且会影响外面该参数的值。这时我们想要修改传值捕捉得到的参数的值,就可以使用 mutable,将它加在参数列表后面表示取消传值捕捉参数的常性,但是在函数体里修改该参数仍不会影响外面的该参数(相当于将这个参数拷贝了一份)。使用 mutable 修饰后,参数列表不可省略(即使参数列表为空,也要带上 ()),用法如下:
auto func7 = [=]()mutable
{
a++;
b++;
c++;
d++;
return a + b + c + d;
};
1.3、lambda 表达式的应用
我们以 sort 函数为例,使用 lambda 表达式:
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price;});
往常,我们的第三个参数应该传一个仿函数(函数对象),由于 lambda 表达式也是可调用对象,所以自然也可以替代仿函数的位置。
注意:lambda 表达式是一个匿名对象,一个可调用的对象,可以把它理解为仿函数。
1.4、lambda 表达式的原理
我们之前说过,将 lambda 表达式当作仿函数理解就可以,其实 lambda 表达式底层实现就是用的仿函数对象,lambda 表达式只是表象,当我们写了这个玩意以后,其实在底层来看我们就是写了一个仿函数。
不同的仿函数类名不同,由于lambda表达式底层也是仿函数,所以在底层,每个lambda表达式都有自己的类名,编译器生成的类名也很简单,就是 lambda 加上一串数字和字母。
捕捉列表的本质是底层仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,对于隐式捕捉,当然不是全都捕捉进捕捉列表,而是使用哪个捕捉哪个。
2、包装器 function
首先我们先来看 std::function ,他也是一个可调用对象(用 () 调用的本质是 () 被进行了函数重载)。
std::function 本质是一个类模板,也是一个包装器。那为什么它被称作包装器呢,其实是因为它的实例化对象能够将其他的可调用对象(比如函数指针,函数,lambda表达式,bind表达式等)包装起来,存储的可调用对象被称为 function 的目标。如果 function 不含有目标,则称它为空。调用空 std::function 的目标导致抛出 std::bad_function_call 异常。
那为什么将其他的可调用对象包装起来呢?其实是因为其他可调用对象的类型五花八门,我们就可以把他们包装起来,统一成一个类型,这就是 function 包装器的妙处,这时声明可调用对象的类型就方便了。
使用 function 需要包上头文件 <functional>,我们来看看它的模板:
//没有定义时
template
class function;
//有定义时
template
class function;
是的,没错,function的参数是可变参数,这里的 Ret 为返回值,可变参数处传你要用的参数类型。只看模板大家可能看不懂,我们给大家看个例子:
class Plus
{
public:
Plus(int n = 10)
:_n(n)
{ }
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return (a + b) * _n;
}
private:
int _n;
};
int main()
{
function f = &Plus::plusi;
cout << f(3, 2) << endl;
}
下面有两个要注意的点:
1、当要包装的是静态成员函数时,一定要指明类域并且在前面加上&(成员函数都要指明类域并加上&),如下:
class Plus
{
public:
Plus(int n = 10)
:_n(n)
{ }
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return (a + b) * _n;
}
private:
int _n;
};
int main()
{
function f = &Plus::plusi;
cout << f(3, 2) << endl;
}
2、当要包装的是普通成员函数时,要记得普通成员函数的第一个参数是那个隐藏的 this 指针。
int main()
{
function f1 = &Plus::plusd;//第一个参数this指针
Plus pa;
cout << f1(&pa, 3.2, 3.3) << endl;
function f2 = &Plus::plusd;//第一个参数可以传对象
Plus pb;
cout << f2(pb, 4.3, 5.4) << endl;
function f3 = &Plus::plusd;//第一个对象可以传对象的引用
Plus pc;
cout << f3(pc, 4.3, 5.4) << endl;
/*function f4 = &Plus::plusd;
Plus pd;
cout << f4(pd, 4.3, 5.4) << endl;*/
//但是不能传const对象,因为this指针为const类型的话就是const成员函数了,要单独有一个const版本的成员函数
}
要想最后一个运行成功,要添加一个成员函数:
class Plus
{
public:
double plust(double a, double b) const//要匹配const成员函数
{
return (a + b) * _n;
}
private:
int _n;
};
int main()
{
function f4 = &Plus::plust;
Plus pd;
cout << f4(pd, 4.3, 5.4) << endl;
}
同样也可以使用右值,如下:
function f5 = &Plus::plusd;
Plus pc;
cout << f5(move(pc), 4.3, 5.4) << endl;
cout << f5(Plus(), 4.3, 5.4) << endl;
3、函数适配器 bind
前面我们讲的 function 是类模板,而这里我们讲的 bind 则是函数模板,他也是一个可调用对象。他是一个函数适配器(也可以看作合成器),他并不是一个对象。
template
bind (Fn&& fn, Args&&... args);//Fn传可调用对象
bind 的作用是把可调用对象包装后返回一个可调用对象,这个可调用对象可以用 auto 对象和合成器 function 对象接收,毕竟合成器 function 的作用不就是接收各种可调用对象嘛。它的头文件也是 <functional>。
调用bind的一般形式: auto newCallable = bind(callable,arg_list); 其中 newCallable 本身是一个可调用对象,arg_list 是一个逗号分隔的参数列表,对应给定的 callable 的参数。当我们调用 newCallable时,newCallable会调用 callable,并传给它 arg_list 中的参数。
arg_list 中的参数可能包含形如 _n 的名字,其中n是一个整数,这些参数是占位符,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的位置。数值 n 表示生成的可调用对象中参数的位置:_1 为 newCallable 的第一个参数,_2为第二个参数,以此类推。_1/_2/_3....这些占位符都放在一个在 placeholders 的命名空间中。
只看这些定义大家可能看不懂,下面来给大家讲一下:
bind 很常用的作用有两个,一是改变可调用对象参数的位置,把参数按照自己的想法调换顺序,如下:
//可以直接展开命名空间 using namespace placeholders,但是这里我们只用三个参数做演示,就不全展开了
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int Sub(int a, int b)
{
return (a - b) * 10;
}
int SubX(int a, int b, int c)
{
return (a - b - c) * 10;
}
int main()
{
cout << Sub(10, 5) << endl;
auto b1 = bind(Sub, _2, _1);
cout << b1(10, 5) << endl;
cout << SubX(10, 2, 3) << endl;
auto b2 = bind(SubX, _2, _3, _1);
cout << b2(10, 2, 3) << endl;
return 0;
}
运行结果如下:

那么看到这里大家可能还是不太明白原理,那我给大家画个图:

第二个作用是改变参数数量,也就是 bind 可以让原可调用对象的某个参数的值固定(把一个值与某个参数绑定)从而达到减少参数的目的,如下:
int SubX(int a, int b, int c)
{
return (a - b - c) * 10;
}
int main()
{
auto b = bind(SubX, _2, 10, _1);
cout << b(5, 35) << endl;
}
运行结果如下:

还是画图分析一下:

当然,我么也可以用 function 对象来接收 bind 的返回值 :
class Plus
{
public:
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
//和function一样,成员函数指明类域并加上&
function f = bind(&Plus::plusd, Plus(), _1, _2);
cout << f(1.1, 1.1) << endl;
}
后记
C++11 添加的内容重要的大部分都讲完了,我还要讲一个智能指针,但是篇幅限制,我要单独放到一篇文章来讲,今天的内容就到这里吧,感谢大家支持。


浙公网安备 33010602011771号