右值引用和Lambda表达式
C++是一个非常追求效率的语言,在C++11中不仅增加了unordered_set和unordered_map形容器,对已有的string、vector、list、map等也增加了移动构造和移动赋值,价值非常大,进一步提高了效率。
1. 右值引用
C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。为了提高程序运行效率
,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。
- 左值(原来所接触的引用):可以放到 “=” 左边的值一般就是可修改的值
- 右值:一般是不可以修改的一些值。比如:常量、临时变量、返回值
#include<iostream>
using namespace std;
int main()
{
int val = 10;
int& lr1 = val; //左值引用一般是给左值取别名,修改lr1也就相当于修改了val
const int& lr2 = int(1); //左值引用也可以给右值取别名,但是右值一般都是不可以修改的值,所以要加上const
int&& rr1 = int(1);//右值引用一般是给右值取别名
int&& rr2 = move(val);//右值引用不能直接给左值取别名,但是可以给move(左值)取别名,c++11增加的移动语义
}
对于上述代码的总结就是:对于一般情况下左值引用就是给左值取别名,但是加上const左值引用也可以给右值取别名。对于右值引用一般是给右值取别名,但是给左值加上move,就相当于改变了左值的属性,使其变为了右值的将亡值,所以右值引用也可以给move后的左值取别名。
1.1 为什么要引入右值引用?
其实右值引用最大的作用是能够让编译器区分出来左值引用还是右值引用,进而让他能够去适配拷贝构造函数还是移动构造函数。
如果一个类中涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符重载以及析构函数,否则编译器将会自动生成一个默认的,如果遇到拷贝对象或者对象之间相互赋值,就会出错,比如:
class String
{
public:
String(char* str = "")
{
if (nullptr == str)
str = "";
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s)
: _str(new char[strlen(s._str) + 1])
{
cout << "String(const String& s)" << endl;
strcpy(_str, s._str);
}
String& operator=(const String& s)
{
cout << "String& operator=(const String& s)" << endl;
if (this != &s)
{
char* pTemp = new char[strlen(s._str) + 1];
strcpy(pTemp, s._str);
delete[] _str;
_str = pTemp;
}
return *this;
}
String operator+(const String& s)
{
char* pTemp = new char[strlen(_str) + strlen(s._str) + 1];
strcpy(pTemp, _str);
strcpy(pTemp + strlen(_str), s._str);
String strRet(pTemp);
return strRet;
}
String& operator+=(const String& s)
{
//......
return *this;
}
~String()
{
if (_str) delete[] _str;
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2("world");
String s3 = s1 + s2; //对于s1和s2为自定义类型,调用operator+,然后返回值在出作用域的时候生命周期将会到,所以要先调用一次拷贝构造
//产生一份临时的,然后在拿这份临时的去拷贝构造s3
cout << endl;
String s4 = s1 += s2;//对于s1和s2为自定义类型,调用operator+=,然后返回的是引用,所以在调用一次拷贝构造
//生成一个临时值然后在调用一次operator=
cout << endl;
String s5;
s5 = s1 += s2;
return 0;
}
- 在operator+中:strRet在按照值返回时,必须创建一个临时对象,临时对象创建好之后,strRet就被销毁了,最后使用返回的临时对象构造s3,s3构造好之后,临时对象就被销毁了。仔细观察会发现:strRet、临时对象、s3每个对象创建后,都有自己独立的空间,而空间中存放内容也都相同,相当于创建了三个内容完全相同的对象,对于空间是一种浪费,程序的效率也会降低,而且临时对象确实作用不是很大,那能否对该种情况进行优化呢?下面将引入右值引用。
结论:传值返回,会多一次拷贝,引用返回,会少一次拷贝(但是从上述代码来看,调用operator+和operator+=好像并没有看出来任何的区别,那是因为编译器进行了优化,实时上,传值返回是会调用两次拷贝构造的,引用返回只需要调用一次拷贝构造)
左值引用作用:
- 函数引用传参—减少拷贝+输出型参数
- 函数引用传返回值—减少拷贝
核心作用:提高效率+提高程序可读性(不需要复杂指针)
//移动构造---移动拷贝
//右值分为1.纯右值2.将亡值
String(String&& s)
:_str(nullptr)
{
cout << "String(String&& s)" << endl;
std::swap(_str, s._str);
}
//移动赋值 --- 移动赋值
String& operator=(String&& s)
{
cout << "String& operator=(String&& s)" << endl;
std::swap(_str, s._str);
return *this;
}
int main()
{
String s1("hello");
String s2("world");
String s3 = s1 + s2; //对于s1和s2为自定义类型,调用operator+,然后返回值在出作用域的时候生命周期将会到,所以要先调用一次拷贝构造
//产生一份临时的,然后在拿这份临时的去拷贝构造s3
cout << endl;
String s4;
s4 = s1 + s2;//对于s1和s2为自定义类型,调用operator+,然后返回值在出作用域的时候生命周期将会到,所以要先调用一次拷贝构造
//然后在调用一次operator=
return 0;
}
C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式。
因为strRet对象的生命周期在创建好临时对象后就结束了,即将亡值,C++11认为其为右值,在用strRet构造临时对象时,就会采用移动构造,即将strRet中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率,这就是右值引用最厉害的地方。
- 涉及了需要深拷贝的类,有了移动构造operator+和operator+=的效率几乎是一样的,还有就是对于std库里面的swap函数和例如vector里面自带的swap,因为移动构造和移动赋值他们在效率方面也就没有什么差别了。
- 如果没有显示写拷贝构造和拷贝赋值,编译器会自动生成移动构造和移动赋值,但是默认生成的移动构造和移动赋值,也完成的是浅拷贝(值拷贝)和拷贝构造和拷贝赋值一样。
注意:
- 移动构造函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。
- 在C++11中,编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造。
1.2 完美转发
简单点说:就是在传参过程中,保持住参数的原有属性不被丢失(是左值还是右值),需要配合std::forward< T >来使用。
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
所谓完美:函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。
void Fun(int &x)
{
cout << "lvalue ref" << endl;
}
void Fun(int &&x)
{
cout << "rvalue ref" << endl;
}
void Fun(const int &x)
{
cout << "const lvalue ref" << endl;
}
void Fun(const int &&x)
{
cout << "const rvalue ref" << endl;
}
template<typename T>
void PerfectForward(T &&t)
{
Fun(std::forward<T>(t)); //完美转发让实参传递的时候可以保持他原有的左右值属性
}
int main() {
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}
2. Lambda表达式
为什么会有这么一系列的能像函数一样使用的语法出来,因为他们各有价值
- 首先C中函数指针有些复杂 void(*pfunc)(int,int),作为类型不方便使用
- C++98中第一步提出仿函数解决,仿函数就是一个类,可以很多作为类型,很多容器算法的比较器都是用的仿函数,但是有时仿函数又不太方便,所以C++11中跟随潮流(其他语言),引入了lambda表达式和包装器。
struct Goods
{
string _name;
double _price; //价格
size_t _saleNum; //销量
//或者评价
};
struct PriceCompare
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price <= gr._price;
}
};
int main()
{
vector<Goods> vg = { { "苹果", 2.1, 100 }, { "香蕉", 3, 200 }, { "橙子", 2.2, 500 }, { "菠萝", 1.5, 80 } };
sort(vg.begin(),vg.end(), PriceCompare());
return 0;
}
随着C++语法的发展,人们开始觉得上面的写法太复杂了,这次我想要根据价格的高低进行排序,然后就写一个仿函数,下一次我想要以saleNum的高低进行排序的时候又要重新写一个仿函数(或者写多个函数,然后sort的第三个参数传不同的函数指针),如果必要我还需要去查看这个仿函数(函数)的具体实现,这些都还是很麻烦,所以就引入了lambda表达式。
2.1 lambda表达式语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
lambda表达式各部分说明
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回 值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获 到的变量。
注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。
struct Goods
{
string _name;
double _price;
int _saleNum;
};
int main()
{
vector<Goods> vg = { { "苹果", 2.1, 100 }, { "香蕉", 3, 200 }, { "橙子", 2.2, 500 }, { "菠萝", 1.5, 80 } };
//sort(vg.begin(),vg.end(), Compare());
sort(vg.begin(), vg.end(), [](Goods& g1, Goods& g2)->double{return g1._price > g2._price; });
sort(vg.begin(), vg.end(), [](Goods& g1, Goods& g2)->size_t{return g1._saleNum > g2._saleNum; });
return 0;
}
会发现清晰明了,不再需要反过来看仿函数的实现,而是直接就能够阅读清楚这个sort是要按照什么来比较。
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调
用,如果想要直接调用,可借助auto将其赋值给一个变量。
捕获列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
auto c1 = [](Goods& g1, Goods& g2)->double{return g1._saleNum > g2._saleNum; };
cout << c1(vg[0], vg[1]) << endl;
lambda表达式的进一步使用
int main()
{
int a = 1, b = 2;
//加的lambda表达式
auto add1 = [](int x, int y)->int{return x + y; };
auto add2 = [a, b]()->int{return a + b; }; //只捕捉a、b
auto add3 = [=]()->int{return a + b; }; //捕捉列表为“=”的时候,就是捕捉外面域的所有参数
cout << add1(a, b) << endl;
cout << add2() << endl;
cout << add3() << endl;
return 0;
}
//交换的lambda表达式
auto swap1 = [](int x, int y)
{
int z = x;
x = y;
y = z;
};
auto swap2 = [&a,&b]()
{
int c = a;
a = b;
b = c;
};
//引用的方式捕捉所有
auto swap3 = [&]()
{
int c = a;
a = b;
b = c;
};
swap1(a, b);
swap2();
swap3();
2.2 包装器
包装器,也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。(什么叫适配器:其实就是把别人的东西拿过来在进行一层封装)
2.2.1 function
简单点说:function可以让很多种函数,都采用统一的方式进行调用。(你可以理解为都把礼物都拿到了同一家店里采用同样的纸张和绷带进行包装,但是里面的东西都是各不同的)
std::function在头文件 类模板原型如下
template < class T> function;
template <class Ret, class… Args>
class function<Ret(Args…)>;
模板参数说明:Ret: 被调用函数的返回类型
Args…:被调用函数的形参
// 使用方法如下:
#include <functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
// 函数名(函数指针)
std::function<int(int, int)> func1 = f;
cout << func1(1, 2) << endl;
// 函数对象
std::function<int(int, int)> func2 = Functor();
cout << func2(1, 2) << endl;
// lamber表达式
std::function<int(int, int)> func3 = [](const int a, const int b) {return a + b; };
cout << func3(1, 2) << endl;
// 类的成员函数 static
std::function<int(int, int)> func4 = Plus::plusi; //因为plusi属于静态成员函数,属于这个类,所以要带上域名
cout << func4(1, 2) << endl;
//因为他要用对象去调用这个成员函数,所以还要传一个匿名对象
//如果我不想要这个Plus参数,就要使用到后面的bind
std::function<double(Plus, double, double)> func5 = &Plus::plusd; //对于成员函数来说,要取地址
cout << func5(Plus(), 1.1, 2.2) << endl;
return 0;
}
对于function这个类是实现了operator bool的,就是可以让类对象直接去做逻辑判断真假。
ret = func(x); 这个func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能 是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
template<class F, class T>
T useF(F f, T x)
{
static int count = 0;
cout << "count:" << ++count << endl;
cout << "count:" << &count << endl;
return f(x);
}
double f(double i)
{
return i / 2;
}
struct Functor
{
double operator()(double d)
{
return d / 3; }
};
int main()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double{ return d/4; }, 11.11) << endl;
return 0;
}
通过上面的程序验证,我们会发现useF函数模板实例化了三份。
有了上面的function就可以做到只让模板实例化出一份,可以大大提高效率。
int main()
{
std::function<double(double)> func1 = f;
cout << useF(func1, 11.11) << endl;
// 函数对象
std::function<double(double)> func2 = Functor();
cout << useF(func2, 11.11) << endl;
// lamber表达式
std::function<double(double)> func3 = [](double d)->double{ return d / 4; };
cout << useF(func3, 11.11) << endl;
return 0;
}
2.2.2 function的应用
LeetCode第150题—逆波兰表达式求值
- 操作数就入栈,操作符就连续取栈顶两个元素计算,计算的结果还需要在入栈。
- lambda表达式 + 包装器
class Solution {
public:
int evalRPN(vector<string>& tokens) {
//遇见不同的操作符,执行不同的函数
map<string,function<int(int,int)>> OpCountMap =
{
{"+",[](int x,int y)->int{return x + y;}},
{"-",[](int x,int y)->int{return x - y;}},
{"*",[](int x,int y)->int{return x * y;}},
{"/",[](int x,int y)->int{return x / y;}}
};
stack<int> st;
for(auto& str : tokens)
{
//这里要分为操作符和操作数的概念
if(str == "+" || str == "-" || str == "*" || str == "/")
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
st.push(OpCountMap[str](left,right));
}
else
{
//c++11所提供的的接口 stoi字符串转整数 还有to_string整数转字符串接口,很好用
st.push(stoi(str));
}
}
return st.top();//最后的结果还会再一次入栈,所以最终返回栈里面的top()就好
}
};
2.2.3 bind
简单说:可以包装一个函数,并对他们的参数进行调整(调整参数的个数或者类型)。
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。
#include <functional>
int Plus(int a, int b)
{
return a + b;
}
class Sub
{
public:
int sub(int a, int b)
{
return a - b;
}
};
int main()
{
//在不改变这个函数情况下,就写成_1,_2,但是他们都是属于 placeholders的命名空间的
auto f1 = bind(Plus, placeholders::_1, placeholders::_2);
function<int(int,int)> f2 = bind(Plus, placeholders::_1, placeholders::_2);
cout << f1(1, 2) << endl;
cout << f2(1, 2) << endl;
//bind的就是把一些固定的参数给邦死,这样就不用每次调用的时候都传
//最大的用处在于bind成员函数的地方,因为需要多传一个对象进去
//bind实际应用,一般是有些函数可能需要一些固定的参数去调用,那么就可以使用bind,把这个参数绑上
function<int(Sub, int, int)> f3 = &Sub::sub; //正常的
cout << f3(Sub(), 1, 2) << endl;
//那有没有方式不传这个Sub对象呢?---bind
function<int(int, int)> f4 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);
cout << f4(1, 2) << endl;
//还可以调整参数的顺序
function<int(int, int)> f5 = bind(&Sub::sub, Sub(), placeholders::_2, placeholders::_1);
cout << f5(1, 2) << endl;
return 0;
}