【C++篇】C++11:从列表初始化到移动语义
目录
前言
编辑
列表初始化
右值引用和移动语义
左值引用和右值引用
左值和右值的参数匹配
移动构造和移动赋值
移动赋值
类型分类
引用折叠
完美转发
可变参数模板
包扩展
emplace系列接口
lambda
捕捉列表
类的新功能
包装器
前言
C++11的发展历史
C++11是C++的第⼆个主要版本,并且是从C++98起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了对C++程序员可用的抽象。在它最终由ISO在2011年8月12日采纳前,⼈们曾使⽤名称“C++0x”,因为它曾被期待在?2010?年之前发布。C++03与C++11期间花了8年时间,故而这是迄今为止最长的版本间隔。从那时起,C++有规律地每3年更新⼀次。
列表初始化
C++98传统的{}
// C++98中⼀般数组和结构体可以⽤{}进⾏初始化。struct Point{int _x;int _y;};int main(){int array1[] = { 1, 2, 3, 4, 5 };int array2[5] = { 0 };Point p = { 1, 2 };return 0;}
C++11中的{}
-
C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化
-
c++11开始,自定义类型支持用初始化列表,c++98只有内置类型支持初始化列表
#include<iostream>#include<vector>using namespace std;struct Point{int _x;int _y;};class Date{public:Date(int year = 1, int month = 1, int day = 1):_year(year), _month(month), _day(day){cout << "Date(int year, int month, int day)" << endl;}Date(const Date& d):_year(d._year), _month(d._month), _day(d._day){cout << "Date(const Date& d)" << endl;}private:int _year;int _month;int _day;};int main(){// C++98⽀持的int a1[] = { 1, 2, 3, 4, 5 };int a2[5] = { 0 };Point p = { 1, 2 };// C++11⽀持的// 内置类型⽀持int x1 = { 2 };// ⾃定义类型⽀持// 这⾥本质是⽤{ 2025, 1, 1}构造⼀个Date临时对象// 临时对象再去拷⻉构造d1,编译器优化后合⼆为⼀变成{ 2025, 1, 1}直接构造初始化Date d1 = { 2025, 1, 1};//C++98⽀持单参数时类型转换,也可以不⽤{}Date d3 = { 2025};//c++11Date d4 = 2025;//c++98//可以省略掉= Point p1 { 1, 2 };int x2 { 2 };Date d6 { 2024, 7, 25 };const Date& d7 { 2024, 7, 25 };//只有初始化列表才支持省略=Date d8 2025//会报错}
C++11中的std::initializer_list
-
上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个 值去构造初始化,那么我们得实现很多个构造函数才能⽀持:vector v1 = {1,2,3};vector v2 = {1,2,3,4,5};
-
C++11库中提出了⼀个std::initializerlist的类, auto il = { 10, 20, 30 }; // the type of il is an initializerlist ,这个类的本质是底层开⼀个数组,将数据拷⻉ 过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。
vector<int> v1={1,2,3,4};
initializer_list<int>l1={10,20,30};//本质是底层在栈上开一个数组,
//这里在语义上表示构造+拷贝构造+优化,,,但编译器会优化成直接构造
//本质也可以理解为隐式类型转换
vector<int> v1={1,2,3,4};
vector<int> v2{1,2,3,4};
//这里在语义上表示直接进行构造
vector<int> v3({1,2,3,4});//调用initializer_list进行构造
//上述两种方式在语义上表示的意思不同,但最后的结果是相同的
右值引用和移动语义
-
左值,是一个数据表达式,一般以持久的状态存储在内存中,可以获取到它的地址。左值可以出现在赋值符号的左边也可以出现在右边
-
右值,是一个数据表达式,要么是字面值常量,要么是表达式求值过程中创建的临时对象。右值不能出现在表达式的左边且无法获取地址。
左值引用和右值引用
-
Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别 名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。
-
左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
-
右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
-
move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换,当然他还涉及⼀些引⽤折叠的知识
-
是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值
double x = 1.1, y = 2.2;
const int& rx1 = 10;
const double& rx2 = x + y;int* p = new int(0);
int b = 1;
string s("111111");
int&& rr1 = move(b);
int*&& rr2 = move(p);
string&& rr3 = move(s);
string&& rr4 = (string&&)s;//move本质是进行强转int& tt1 = rr1;//用左值引用来引用右值引用表达式
左值引用与右值引用在底层其实就是指针
引用延长生命周期
右值引用可用于为临时对象延长生命周期,const的左值引用也能延长临时对象生存期,但这些对象无法被修改。
如果想用引用来延长被调用的函数内部局部变量的生命周期,这是不被允许的。第一点:引用不会改变变量的存储位置。第二点:局部变量是创建在函数栈帧中的,当函数调用结束栈帧销毁,局部变量也会随之销毁。
string s1 = "test";
//string&& r1 = s1;//右值引用无法引用左值const string& r2 = s1 + s1;
//r2 += s1;//const左值可以引用右值,但无法进行修改string&& r3 = s1 + s1;
r3 += s1;
cout << r3 << endl;
左值和右值的参数匹配
-
C++98中,我们实现⼀个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
-
C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数,那么实参是左值会匹配f(左值引用),实参是const左值会匹配f(const左值引用),实参是右值会匹配f(右值引用)。
void func(int& x)
{cout << "左值引用" << x <<endl;
}void func(const int& x)
{cout << "const左值引用" << x << endl;
}void func(int&& x)
{cout << "右值引用" << x<<endl;
}int main()
{int i = 1;const int ci = 2;func(i);func(ci);func(3);func(move(i));int&& x = 1;func(x);func(move(x));return 0;
}
左值引用与右值引用最终目的是减少拷贝、提高效率。
左值引用还可以修改参数或者返回值,方便使用
左值引用的不足:
在部分函数场景,只能传值返回,不能传引用返回。比如:当前函数的局部对象,出了当前函数的作用域就销毁
移动构造和移动赋值
移动构造函数是⼀种构造函数,类似拷⻉构造函数,要求第⼀个参数是该类类型的引⽤,不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
移动构造是进行指针交换,其本质是“掠夺资源”。被掠夺的右值的指针则指向”空“
所以一个左值不能轻易的去move,因为这会导致左值的资源被掠夺
右值对象构造,只有拷贝构造,没有移动构造的场景
- vs2019debug环境下编译器对拷贝进行了优化。当移动构造与拷贝构造同时存在时,编译器会选择代价小的移动构造。优化前,需要进行两次移动构造,优化后只需进行一次移动构造
- 需要注意的是在vs2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为⼀,变为直接构造。变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解
- linux下可以将下面代码拷贝到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elide-
constructors 的方式关闭构造优化,运行结果可以看到图1左边没有优化的两次拷贝。
右值对象构造,有拷贝构造,也有移动构造的场景
- 展示了vs2019debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次移动构造。
- 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为⼀,变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的角度理解
- linux下可以将下⾯代码拷贝到test.cpp⽂件,编译时用g++ test.cpp -fno-elide-
constructors 的方式关闭构造优化,运行结果可以看到图1左边没有优化的两次移动。
右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
-
左边展示了vs2019?debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值。
- 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
- 展示了vs2019debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次移动构造,⼀次移动赋值。
- 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
如果想看未优化的场景,在Linux下通过:g++ test.cpp -fno-elide-constructors关闭构造优化来观察。
移动赋值
-
移动赋值是⼀个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
-
移动赋值也是对资源进行掠夺
只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
左值调用拷贝构造,因为左值数据不能轻易改动,可能会影响到后面的程序。
右值调用移动构造,因为右值生命周期极短,比起拷贝构造,用移动构造付出的代价更小,并且效率更高
类型分类
右值:
-
纯右值
-
将亡值
泛左值:
-
左值
-
将亡值
引用折叠
-
C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或typedef 中的类型操作可以构成引⽤的引⽤。
-
通过模板或typedef中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
typedef int& lref;typedef int&& rref;int n = 0;//只有当是右值右值时,才是右值引用。有左则是左lref& r1 = n;lref&& r2 = n;rref& r3 = n;rref&& r4 = 1;
------------------------------------------------------------template<class T>void f1(T& x){}template<class T>void f2(T&& x){ }int main(){typedef int& lref;typedef int&& rref;int n = 0;lref& r1 = n;lref&& r2 = n;rref& r3 = n;rref&& r4 = 1;f1<int>(n);//这里没有引用折叠//f1<int>(0);//是左值类型,所以不能引用右值f1<int&>(n);//f1<int&>(0);//调用的是f1,因为引用折叠,所以f1只可能是左值f1<int&&>(n);//f1<int&&>(0);////调用的是f1,因为引用折叠,所以f1只可能是左值f1<const int&>(n);f1<const int&>(0);//const 左值可以引用右值f1<const int&&>(n);f1<const int&&>(0);//因为引用折叠,所以推出是左值类型,const左值可以引用右值//f2<int>(n);//这里没有引用折叠,所以推出类型是右值,右值不能引用左值f2<int>(0);f2<int&>(n);//f2<int&>(0);存在引用折叠,所以有左则是左,所以推出是左值类型,不能引用右值//f2<int&&>(n);//存在引用折叠,全右则右,所以推出是右值类型,右值引用不能引用左值f2<int&&>(0);return 0;}
完美转发
-
Function(T&&t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化 以后是右值引用的Function函数。
-
变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传 递给下⼀层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性, 就需要使用完美转发实现
template<class T>
void func2(T&& z)
{cout << z << endl;
}template<class T>
void func1(T&& t)
{//func2(t);因为右值引用表达式属性是左值,如果没调用完美转发,则传给func2的参数的属性是左值func2(forward<T>(t));//调用完美转发,则会维护参数的属性,传递给func2的参数的属性则保持为右值cout << t << endl;
}int main()
{int x = 1;func1(1);return 0;
}
在没有调用完美转发时,函数调用的结果:
在Function函数内部推出了参数的类型,但是由于右值引用表达式是左值属性,所以在调用Func函数时,调用的是左值引用的函数。
在调用完美转发后,函数调用结果:
完美转发维护了Function函数内部传给Func函数参数的属性,所以右值能调用到右值引用的函数
可变参数模板
-
C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包。我们⽤省略号来指出模板参数或函数参数包。
-
在模板参数列表中,class…或 typename…指出接下来的参数表⽰零或多个类型列表;在函数参数列表中,类型名后⾯跟…指出 接下来表⽰零或多个形参对象列表
-
可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
-
这⾥我们可以使用sizeof…运算符去计算参数包中参数的个数
-
模板参数包:表⽰零或多个模板参数
-
函数参数包:表⽰零或多个函数参数
//前面是模板参数包,后面是函数参数包
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}
计算参数包中参数的个数:
template<class ...Args>
//前提:有模板语法支持
//这里是一个万能引用+可变参数模板+引用折叠。编译器会根据传入的参数实例化出对应的函数
void Print(Args&&...args)
{cout << sizeof...(args) << endl;
}
int main()
{int x = 1;string s1("1");double y = 1.1;Print();Print(1);Print(1,s1,y);return 0;
}
------------------------------
在使用可变参数模板时,从语法层面我们可以理解为:
函数模板:一个函数模板,可以实例化出多个不同类型的函数
可变参数模板:一个可变参数模板函数,可以实例化出多个参数个数不同的函数模板
包扩展
包扩展:解析一个包就叫做包扩展
void ShowList()//当包中的参数个数为0时,调用这个函数
{cout << endl;
}template<class T,class ...Args>
void ShowList(T x,Args...args)//将参数包的第一个参数匹配给x,再将参数个数为n-1的参数包匹配给args
{/*这种写法是错误的,因为包展开匹配解析是在编译时进行的,而if判断是在程序运行时进行的if(sizeof...(args)==0)return;*/cout << x << " ";ShowList(args...);
}template<class ...Args>
void Print(Args&&...args)
{//cout << sizeof...(args) << endl;ShowList(args...);}int main()
{Print(1,string("22222"),2.2);return 0;
}
包扩展详细过程第一种方法:
编译时递归
将包扩展的过程展开写就是:
void ShowList()
{cout << endl;
}void ShowList( double z)
{cout << z << " ";ShowList();
}void ShowList(string y, double z)
{cout << y<< " ";ShowList( z);
}void ShowList(int x,string y,double z)
{cout << x << " ";ShowList(y,z);
}template<class ...Args>
void Print(Args&&...args)
{//cout << sizeof...(args) << endl;ShowList(args...);
}int main()
{Print(1,string("22222"),2.2);return 0;
}
模板是写给编译器的,模板实例化的过程交给编译器来完成,方便程序员
包扩展详细过程第二种方法:
template<class T>
const T& GetArs(const T& x)
{cout << x << " ";return x;
}template<class ...Args>
void Arguments(Args...args)
{ }template<class ...Args>
void Print(Args&&...args)
{Arguments(GetArs(args)...);、/*void Print(int x,string y,double z){Arguments(GetArs(x),GetArs(y),GetArs(z));}*/
}int main()
{Print(1,string("22222"),2.2);return 0;
}-----------------------------------/*不能写成如下这样,因为这不符合c++语法规则。参数包展开(args...)必须发生在允许的上下文环境中如:函数调用参数、初始化列表或模板参数列表等地方。单独的GetArs(args)...;会被解析为试图展开多个表达式语句,这在语法上是不合法的。*/
template<class ...Args>
void Print(Args&&...args)
{GetArs(args)...;
}
emplace系列接口
emplace_back:
-
传入左值,调用构造和拷贝构造
-
传入右值,调用构造和移动构造
push_back:
-
传入左值,调用构造和拷贝构造
-
传入右值,调用构造和移动构造
两者区别:
当是以隐式类型转换的方式传入值时:
emplace_back是直接构造
push_back是调用构造在调用移动构造。
当插入的参数是多参数时,push_back()须使用make_pair,而emplace_back则不需要使用make_pair
emplace系列兼容push系列和insert的功能,部分场景下emplace可以直接构造,push和insert则是调用构造+移动构造/拷贝构造。 所以emplace系列接口综合而言更强大
lambda
-
lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。
-
lambda 表达式语法使用层而言没有类型,所以我们⼀般是用auto或者模板参数定义的对象去接收lambda对象
lambda表达式的格式:
[capture-list] (parameters)-> return type {function boby }
-
[capture-list]:捕捉列表,该列表总是出现在lambda函数的开始位置, 编译器根据[]来判断接下来的代码是否为lambda 函数。
-
(parameters):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()⼀起省略
-
->return type::返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此 部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导
-
{function boby}::函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以 使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
/*捕捉列表不能省略参数列表与返回值可以省略函数体不能省略
*/
int main()
{auto add1 = [](int x, int y)->int {return x + y; };cout << add1(1,2) << endl;auto func1 = []{cout << "hello world" << endl;return 0;};func1();return 0;
}
捕捉列表
lambda 表达式中默认只能⽤lambda 函数体和参数中的变量,如果想⽤外层作⽤域中的变量就需要进行捕捉。
捕捉方法:
-
显示的传值捕捉和传引用捕捉,[x, y,&z]表⽰x和y传值捕捉,z传引用捕捉。
-
捕捉列表中隐式捕捉:捕捉列表写⼀个=表示隐式传值捕捉,捕捉列表写⼀个&表示隐式传引用捕捉。并不是捕捉全部的变量,而是用哪个变量捕捉哪个变量
-
捕捉列表中混合使用隐式捕捉和显示捕捉,[=,&x]表示其他变量隐式值捕捉, x引用捕捉;[&,x,y]表示其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第⼀个元素必须是 &或=。
同一个变量不能捕捉两次
值捕捉的变量不能修改,因为值捕捉的变量相当于默认加了const
引用捕捉允许对变量进行修改
类的新功能
- 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重 载/const 取地址重载,C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载
- 没有最近实现移动构造函数,且没有实现析构函数、拷⻉构造、拷贝赋值重载中的任意⼀ 个。那么编译器会自动生成⼀个默认移动构造。
-
默认⽣成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝
-
⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调⽤拷⻉构造。
namespace liu
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "string(char* str)构造" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}//构造string(const string& s){cout << "string(const string& s) -- 拷⻉构造" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}}// 移动构造string(string&& s){cout << "string(string&& s) -- 移动构造" << endl;swap(s);}string& operator=(const string& s){cout << "string& operator=(const string& s) -- 拷⻉赋值 " <<endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s){push_back(ch);}}return *this;}// 移动赋值string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;swap(s);return *this;}~string(){//cout << "~string() -- 析构" << endl;delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];if (_str){strcpy(tmp, _str);delete[] _str;}_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity *2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}size_t size() const{return _size;}private:char* _str =new char('\0');size_t _size = 0;size_t _capacity = 0;};
};class Person
{
public:Person(const char*name="张三", int age=1):_name(name),_age(age){ }~Person(){}private:liu::string _name;int _age;
};int main()
{//如果Person类中没有实现析构函数、拷贝构造、拷贝赋值重载,那么编译器会自己生成一个移动构造,由于Person类中包含了string类,string类中实现了移动构造,所以直接调用string类中的移动构造Person s1;Person s2=s1;Person s3=(move(s2));//因为Person类中实现了析构函数,所以编译器不会自己生成移动赋值,哪怕string类中实现了移动赋值,也不会调用。Person s4;s4 = move(s2);return 0;
}
包装器
std::function 是⼀个类模板,也是⼀个包装器。std::function 的实例对象可以包装存储其他的可以调用对象:
-
函数指针
-
仿函数
-
lambda
-
bind
存储的可调用对象称为:function的目标,若没有目标,则称为空,调⽤空 std::function 的⽬标导致抛出std::bad_function_call异常
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();//包装lamdbafunction<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;//包装静态成员函数,需要指定类域function<int(int, int)>f4 = Plus::plusi;cout << f4(1,1) << endl;//包装非静态成员函数还需传入this指针function<int(Plus*,double, double) > f5 = &Plus::plusd;Plus pl;cout << f5(&pl, 1, 1) << endl;//包装非静态成员函数也可以像如下这样,因为this本质是不允许显示传递的。//传入Plus*其实是用指针调用成员函数,传入Plus是用对象调用成员函数//调用成员函数其实是使用".*"操作符进行调用function<double(Plus, double, double) > f6 = &Plus::plusd;cout << f6(pl, 1.1, 1.1) << endl;//包装非静态成员函数也可以像如下这样。也可也使用左值引用,只不过使用左值引用就不能传入Plus()匿名对象了function<double(Plus&&, double, double)> f7 = &Plus::plusd;cout << f7(move(pl), 1.1, 1.1) << endl;cout << f7(Plus(), 1.1, 1.1) << endl;return 0;
}