C++11新特性:lambda表达式,包装器,新的类功能
1. lambda表达式
1.1 基本语法
lambda表达式本质上是一个匿名函数对象,但是和普通函数不一样他可以定义在函数内部。
lambda表达式使用层而言没有类型,所以我们一般使用auto或者模板参数定义的对象去接收lambda对象。
lambda表达式的格式如下:
[ capture-list ] (parameters)-> return type{function body}
- [capture-list]:捕捉列表,该列表在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量来给lambda函数使用,详细下面说明,捕捉列表可以为空但是不能省略。
- ( parameters ):参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()一起省略。
- ->return type:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。一般返回值类型明确的情况下,也可省略,由编译器对返回类型进行推导。
- { funct body }:函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
#include<iostream>
using namespace std;
int main()
{
auto fun1 = [](int x1, int x2)-> int {cout << "222" << endl; return x1 + x2; };
cout << fun1(1, 2) << endl;
//1.捕捉为空也不能省略
//2.参数为空可以省略
//3.返回值为空可以省略,并且可以通过返回对象自动推导
//4.函数体是不可以省略的
//
auto fun2 = []
{
cout << "hello world" << endl;
return 0;
};
cout << fun2() << endl;
int a = 20, b = 10;
auto swap1 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap1(a, b);
cout << a << b<< endl;
return 0;
}
1.2 捕捉列表
lambda表达式中默认只能用lambda函数体和参数中的变量,如果想用外层作用域中的变量就需要捕捉了。
第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。[a,b,&c]表示a和b是值捕捉,c是引用捕捉。会改变c的值
void test2()
{
//只能用当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3, e = 4;
auto func1 = [a, b, &c]
{
//值捕捉的变量是不可修改的,引用捕捉的变量可以修改。
//a++;
c++;
int sum = a + b + c;
return sum;
};
cout << "c的值为:" << c << endl;
cout << func1() << endl;
}
第二种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个= 表示隐式值捕捉,在捕捉列表写一个&表示隐式引用捕捉,这样我们lambda表达式中用了哪些变量,编译器就会自动捕捉那些变量。
void test2()
{
//只能用当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3, e = 4;
auto func2 = [=] ()-> int
{
//值引用,不可以修改
//a++;
int sum = a + b + c;
return sum;
};
cout << func2() << endl;
auto func3 = [&]
{
a++;
b++;
c++;
int sum = a + b + c;
return sum;
};
cout << func3() << endl;
}
第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=,&a]表示其他变量隐式值捕捉,a引用捕捉;[&,a,b],表示其他变量引用捕捉,a,b值捕捉。当使用混合捕捉时,第一个元素必须是&或者=,并且&混合捕捉时后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后面的捕捉变量必须是引用捕捉。
void test2()
{
//只能用当前lambda局部域和捕捉的对象和全局对象
int a = 0, b = 1, c = 2, d = 3, e = 4;
auto func4 = [=, &a]
{
//值捕捉不能修改
//b++;
a++;
int sum = a + b + c;
return sum;
};
cout << func4() << endl;
auto func5 = [&, a, b]
{
//值捕捉不能修改
//a++;
c++;
int sum = a + b + c;
return sum;
};
cout << func5() << endl;
}
lambda表达式如果在函数局部域中,他可以捕捉lambda位置之前定义的变量,不可以捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda表达式中可以直接使用。这也意味着lambda表达式如果定义在全局位置,捕捉列表必须为空。
默认情况下,lambda捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改,mutable加在参数列表的后面可以取消其常量性,也就是说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改的还是形参对象,不会影响实参。
void test3()
{
int a = 0, b = 1, c = 2;
//传值捕捉本质是一种拷贝,并且被const修饰了
//mutable相当于去掉了const属性,可以修改了
//但是修改了不会影响外面被捕捉的值,因为是一种拷贝
//
auto func1 = [=]()mutable
{
a++;
b++;
c++;
return a + b + c;
};
cout << "a的值为" << a << endl;
cout << "b的值为" << b << endl;
cout << "c的值为" << c << endl;
}
1.3 lambda表达式的应用
在学习lambda表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用lambda去定义可调用对象,既简单又方便。
#include<iostream>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;
struct Boy
{
string _name;
int _age;
string _id;
Boy(const string s1, int p, const string s2)
:_name(s1)
, _age(p)
, _id(s2)
{
}
};
struct CompareAge
{
bool operator() (const Boy& b1, const Boy& b2)
{
return b1._age < b2._age;
}
};
struct CompareId
{
bool operator() (const Boy& b1, const Boy& b2)
{
return b1._id < b2._id;
}
};
int main()
{
vector<Boy> v = { {"张三",17,"561325"},{"李四",19,"603149"},{"王五",18,"264146"} };
sort(v.begin(), v.end(), CompareAge());
for (auto e : v)
{
cout << e._age << endl;
}
sort(v.begin(), v.end(), CompareId());
for (auto e : v)
{
cout << e._id << endl;
}
sort(v.begin(), v.end(), [](const Boy& b1, const Boy& b2) {return b1._age > b2._age; });
for (auto e : v)
{
cout << e._age << endl;
}
sort(v.begin(), v.end(), [](const Boy& b1, const Boy& b2) {return b1._id > b2._id; });
for (auto e : v)
{
cout << e._id << endl;
}
return 0;
}
1.4 lambda的原理
lambda的原理和范围for类似,编译后从汇编指令层来看,压根就没有lambda和范围for。范围for的底层是迭代器实现,lambda底层是仿函数对象,也就是说我们写了一个lambda以后,编译器会生成一个对应的仿函数的类。
仿函数的类型是编译时按照一定规则生成的,保证不同的lambda生成的类名不同,lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体,lambda的捕捉列表本质上是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传哪些对象。
我们通过汇编看一下
struct Boy
{
string _name;
int _age;
string _id;
Boy(const string s1, int p, const string s2)
:_name(s1)
, _age(p)
, _id(s2)
{
}
};
struct CompareAge
{
bool operator() (const Boy& b1, const Boy& b2)
{
return b1._age < b2._age;
}
};
int main()
{
vector<Boy> v = { {"张三",17,"561325"},{"李四",19,"603149"},{"王五",18,"264146"} };
auto func1 = [](const Boy& b1, const Boy& b2) {return b1._id > b2._id; };
sort(v.begin(), v.end(), CompareAge());
sort(v.begin(), v.end(),func1 );
return 0;
}
在两个操作的最后一步call,都是调用operator(),lambda表达式类型是lambda_1,这个类型名的规则是编译器自己定制的,保证不同的lambda不冲突。
2. 新的类功能
2.1 默认的构造
原来C++类中,有6个默认的成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/const取地址重载,最后重要的是前四个,后两个用处不是很大,默认成员函数就是我们不写编译器会生成一个默认的。C++新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
当你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
当你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任一个,那么编译器就会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐个成员按字节拷贝,自定义类型成员,则看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(和上面移动构造完全类似)。
2.2 defult和delete
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成默认构造了,我们可以通过default关键字显示指定移动构造生成。
#include<iostream>
#include<algorithm>
using namespace std;
class Person
{
public:
Person() = default;
Person(const char* name, int age)
:_name()
,_age(age)
{
}
Person(const Person& p) = default;
Person(Person&& p) = default;
private:
string _name;
int _age;
};
int main()
{
Person p1;
Person p2=p1;
Person p3=move(p1);
return 0;
}
如果想要限制某些默认函数的生成,在C++11中只需要在该函数声明加上=delete即可,该语法让编译器不生成对应函数的默认版本,将=delete修饰的函数为删除函数。
class Person
{
public:
//Person() = delete;
Person(Person&& p) = delete;
private:
string _name;
int _age;
};
2.3 final与override
之前讲过这里简单介绍一下
final:修饰虚函数,表示虚函数不能再被重写。
override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
3. 包装器
3.1 function
std::function是一个类模板也是一个包装器。std::function的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、lambda、bind表达式等。所存储的可调用对象被称为std::function的目标。若std::function不含目标,则称它为空。调用空std::function的目标会导致抛异常std::bad_function_call异常。
function的原型如下,被定义在<functional>头文件中。
template <class T>
class function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
函数指针、仿函数、lambda等可调用对象的类型各不相同,std::function的优势就是统一类型,对他们都可以包装,这样在很多地方就方便声明可调用对象的类型。
#include<iostream>
#include<functional>
using namespace std;
int add(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
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;
};
void Print()
{
cout << "hello world" << endl;
}
包装各种可调用对象
int main()
{
function<int(int, int)> f1 = add;
function<int(int, int)> f2 = Functor();
function<int(int, int)> f3 = [](int a, int b) {return a + b; };
function<void()> f4 = Print;
f4();
cout << f1(1, 1)<<endl;
cout << f2(1, 1)<<endl;
cout << f3(1, 1)<<endl;
}
包装静态成员函数
成员函数要指定类域明确前面加&才能获取地址
function<int(int, int)> f5 = &Plus::plusi;
cout << f5(1, 1) << endl;
包装不同成员函数
普通成员函数还有一个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以
Plus p;
function<double(Plus*, double, double)> f6 = &Plus::plusd;
cout << f6(&p, 2.2, 2.2) << endl;
function<double(Plus, double, double)> f7 = &Plus::plusd;
cout << f7(p, 2.2, 2.2) << endl;
function<double(Plus&&, double, double)> f8 = &Plus::plusd;
cout << f8(move(p), 2.2, 2.2) << endl;
3.2 bind
bind是一个函数模板,它也是一个可调用对象的包装器,可以把他看做一个函数适配器,对接收的fn可调用对象进行处理后返回一个可调用对象。bind可以用来调整参数个数和参数的顺序。
bind也在<functional>这个头文件中。
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
调用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的一个命名空间中。
#include<functional>
int sub(int a, int b)
{
return a - b;
}
int threesub(int a,int b,int c)
{
return a - b - c;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a - b;
}
};
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;
};
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int main()
{
return 0;
}
bind来改变参数顺序
auto b1 = bind(sub, _1, _2);
cout << b1(3, 1) << endl;
//bind本质上返回一个仿函数对象
// 调整参数顺序0
// _1代表第一个实参
// _2代表第二个实参
auto b2 = bind(sub, _2, _1);
cout << b2(3, 1) << endl;
调整参数个数
auto b3 = bind(sub, 100, _1);
cout << b3(10) << endl;
auto b4 = bind(sub, _1, 100);
cout << b4(10) << endl;
auto b5 = bind(threesub, _1, _2, 100);
cout << b5(5, 1) << endl;
auto b6 = bind(threesub,100, _1, _2);
cout << b6(5, 1) << endl;
auto b7 = bind(threesub, _1, 100, _2);
cout << b7(5, 1) << endl;
还可以对成员函数对象进行绑定,就不需要每次都传递参数了
function<double(Plus&&, double, double)> ff1 = &Plus::plusd;
Plus p;
cout << ff1(move(p), 1.1, 1.1) << endl;
cout << ff1(Plus(), 1.1, 1.1) << endl;
function<double(double, double)> ff2 = bind(&Plus::plusd, Plus(), _1, _2);
cout << ff2(1.1, 1.1) << endl;
实践中设计一个计算利率的lambda表达式,也可以绑定一些参数来进行计算,不同年利率,不同存储年份。
auto func1 = [](double rate, double money, int year) {
double ret = money;
for (int i = 0; i < year; i++)
{
ret += ret * rate;
}
return ret - money;
};
// 绑死⼀些参数,实现出⽀持不同年华利率,不同⾦额和不同年份计算出复利的结算利息
function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);
function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);
function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);
function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);
cout << func3_1_5(1000000) << endl;
cout << func5_1_5(1000000) << endl;
cout << func10_2_5(1000000) << endl;
cout << func20_3_5(1000000) << endl;
这篇就到这里啦(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤
ヾ( ̄▽ ̄)Bye~Bye~