[](){}
是C++什么特性
原文再续就书接上一回。
上一回我们讲到了shared_ptr通过调用可调用对象进行内存的释放。那么对于可调用对象我们知道多少?
可调用对象都有哪些?
C++语言中有5种可调用对象,在这里将会进行一一介绍。包括: 函数、函数指针、lambda表达式、bind创建的对象以及重载了函数调用运算符的类。 和其他对象一样,可调用对象也有类型。例如lambdayou 自己唯一的(未命名)类类型;函数以及函数指针的类型则由返回值类型和实参类型决定等。
顾名思义,函数也算是一种可调用对象,补充一点,函数的类型由它的返回类型和形参类型共同决定,与函数名无关。那我们来看看实例
void funcf(Node* p){
cout << "hello world" << endl;
}
shared_ptr<Node> bar(new Node(),funcf);
为森么可以这样用呢?为什么可以直接将funcf调用,我们首先清楚一点,和函数调用,就是由函数生成的代码置于某块内存中的地址被调用的过程。所以,funcf间接地成为了这块地址的指针。也就是我们说的函数指针,那函数指针的具体定义是什么?
函数指针指向的是函数而非对象。和指针一样,函数指针指向某些特定类型。先来看一个🌰
bool lengthCompare(const string& a,const string& b){
return a.size()>b.size();
}
bool (*pf)(const string&,const string&);
int main(void){
pf = lengthCompare;
if(pf("rrrs","sss")){
cout << "yes"<<endl;
};
return 0;
}
我们可以看到pf是一个函数指针,他只想一个函数,该函数的参数是两个const string的引用。我们声明的是函数指针,指向的必须是一个同样的返回类型以及参数类型的函数(需要精准匹配)。
那怎么使用呢?代码中的使用是直接指向某个函数,而且这个函数名其实也充当着指向某块内存的作用,所以这里其实相当于给类型为bool的指针赋值。
也可以这样使用
pf = &lengthCompare;
bool b1 = (*pf)("aaa","bbbb");
那么如果函数重载呢?函数指针会怎么选择?其实这里并不存在选择的问题,因为函数指针必须精准匹配。
void ff(int *);
void ff(unsigned int);
void(*pf)(unsigned int) = ff; // 这里必须精准匹配
那么既然一般的指针可以做形参使用,那函数指针呢?如前所述,这也是一个指针,当然可以当作形参来使用。
void usePointer(const string& s1,const string& s2,bool (*pf)(const string&,const string&));
usePointer(s1,s2,lengthCompare);
如果嫌弃这个指针的声明过于冗长,那么我们可以使用typedef来简化。
typedef bool Func(const string&,const string&);
typedef decltype(lengthCompare) Func2;
typedef bool (*Funcp)(const string&,const string&);
typedef decltype(lengthCompare) *FuncP2;
所以可以这样使用
void usePointer(const string&,const string&,Func);
void usePointer(const string&,const string&,Func2);
既然可以传入函数指针,那么返回这个函数的指针呢?
using PF = bool(*) (const string&,const string&);
PF f1(const int& a){
cout << a << endl;
return lengthCompare;
}
bool (*pf1)(const string&,const string&);
pf1 = f1(9);
pf1("sss","ddd");
所以上面使用了lengthCompare作为返回的指针,如代码中这样使用。
返回指向函数的指针我们说明了,哪还有别的吗?也可以用尾置返回函数指针。
auto f11(const int& a)->bool(*)(const string&,const string&){
cout << a << endl;
return lengthCompare;
}
bool (*pf1)(const string&,const string&);
pf1 = f11(10);
pf1("eeee","sssssss");
尾置返回类型,任何函数的定义都能使用尾置返回。这种新标准针对于返回类型比较复杂的函数而设计的。比如返回指针数组类型
auto func(int i)->int(*)[10];
在本应该出现返回类型的地方放置了一个auto。这是显式地指明返回类型。
返回类型有个例子比较重要,这里贴一下
decltype(odd) *aPtr(int i){
return (i%2)?& odd:& even;
}
所以继续我们的函数指针,这个可调用对象在shared_ptr中的使用
void funcf(Node* p){
cout << "hello world" << endl;
}
void (*pf)(Node *p);
...
pf = funcf;
shared_ptr<Node> bar(new Node(),pf); // 函数指针
那么五个可调用对象我们讲了两个,一个是函数,一个是函数指针,接下来我们讲一下lambda表达式
我们先来了解一下谓词
什么是谓词?谓词是一个可调用表达式,其返回结果是一个能用作条件的值。
标准算法所使用的谓词分为一元谓词(接受一个参数)和二元谓词(接受两个参数)
bool isShorter(const string& a,const string& b){
return a.size()< b.size();
}
sort(words.begin(),words.end(),isShorter); // 这里就是二元谓词
接受谓词参数的算法对输入序列中的元素调用谓词。因此元素类型必须能转换为谓词的参数类型。
猜测,算法实现中会有这样的模版
template<typename T> class sort{
...
sort(T::iter,T::iter,bool (*pf)(const T& a,const T& b));
}
还有其他的定义,估计,例如限制谓词的参数长度不大于2。
那么我们如果需要传入更多的参数呢?这时候就需要lambda表达式。
我们先来介绍一下lambda表达式,这也是一个可调用对象,我们可以向一个算法传递任何类别的可调用。
对于一个对象或表达式,如果可以对其使用调用运算符()
,则称它为可调用的。
使用官方的解释,一个lambda表达式表示一个可调用的代码单元。(理解为一个未命名的内联函数)
[capture list](parameter list) -> return type { function body} // 这是标准的形式
一个lambda表达式包含
- 一个可能为空的capture list[],指明定义环境中的那些名字能被用在lambda表达式内,以及这些名字的访问方式是拷贝(=)还是引用(&)。
- 一个可选的(parameter list)指明lambda表达式所需的参数
- 一个可选的mutable修饰符 指明lambda表达式可能会修改它自身的状态(即改变通过值捕获的变量的副本)
[v]()mutable{return ++v1;};
- 一个可选的noexcept修饰符
- 一个可选的->返回类型声明 lambda的返回值必须用->尾置返回指定返回类型
- 一个表达式体
先来看一下一个简单的lambda表达式的使用
auto f = []{return 42;};
cout << f() << endl;
也是够简单的,但是为什么会有调用运算符呢?其实上面的代码可以看成下面这一段
class lambda1{
public:
int operator()(){
return 42;
}
};
lambda1 lam1;
cout << lam1() << endl;
这就是原理
接下来向lambda传递参数,也就是可选的参数列表里面的参数
stable_sort(words.begin(),words.end(),[](const string& a,const string& b){return a.size()>b.size();});
其实lambda就是一个未命名的类,可以看作下面的实现方式
class lambda2{
public:
bool operator()(const string& a,const string& b){
return a.size()>b.size();
}
}
调用运算符的参数就是lamda表达式的形参列表。看完了形参列表我们再来看一下这个捕获列表
void print_modulo(const vector<int>& v,ostream& os,int m){
for_each(begin(v),end(v),[&os,m](int x){if(x%m==0) os<< x << '\n';});
}
// 等价于
class lambda3{
ostream& os;
int m;
public:
lambda3(ostream& s,int m):os(s),m(mm){} // 捕获列表
void operator()(int x)const{
if(x%m==0) os<< x<<'\n'; // m是个全局的变量
}
}
捕获分为6类
[], [&], [=], [capture_list], [&,capture_list], [=,capture_list]
上面出了lambda3之外,都是空捕获,那么什么是捕获呢?[]是lambda的引入符号,如果我们需要在lambda内部需要访问外部的变量就需要捕获。
我们先来看看引用捕获和值捕获有什么区别。先来看一段代码
void print(){
int i = 42;
auto f = [&i](){return i;}
++i;// 改变了i 引用捕获,f()也会改变为43 f保存的是i的引用而非拷贝
}
void print2(){
int i = 42;
auto f = [i](){return i;}
++i; // 改变了i 值捕获但是f()并没有改变 f保存的是i的副本
}
以上就是两个捕获,一个是引用捕获,保存的是i的别名,而另一个是值捕获,保存的是i的副本。
所以这两个混合使用就是我们lambda3的效果。我们继续来看后面几个捕获
拥有多个捕获参数的🌰
void print(){
int i = 42;
int ir = 43;
int ie = 44;
auto f = [&i,&ir,&ie](){i = ir+ie;return i;};
++ir;
f();
cout << i << endl;
}
// auto f = [&i,&ir,ie](){i = ir+ie;return i;};也可以这样,但是ie只是一个副本
这里改变了i的值,因为通过引用访问,所以改变了i的值。
那么我们继续来看看只有一个[&]或一个[=]的情况。
int i = 42;
auto f = [&](){return ++i;};
f();
让编译器去推断捕获方式以及参数。名字必须与外部的变量相同,值捕获同理
除了如此,捕获列表还能这样使用[&,capture_list]。我们来看看例子
auto f = [&,ir](){i = ir+ie;return i;};
f();
cout << i << endl;
对于名字没有出现在捕获列表中的局部变量,通过引用隐式捕获,列表中可以出现this或紧跟这...的名字以表示元素。
看看lambda和this的使用
class Request{
function<map<string,string>(const map<string,string>&)> oper;
map<string,string> values;
map<string,string> results;
public:
Request(const string& s);
void execute(){
[this](){ results = oper(values);};//根据结果执行相应的操作 相当于执行this.oper,this.results
}
};
但是这里的function又是怎样一回事,啥玩意?
function是标准库类型,通过指明返回类型和参数类型来说明。
🌰
int f(double a){
++a;
return 0;
}
function<int(double)> fct{f};
function是一种类型,它可以保存你能用调用运算符()调用的任何对象。也就是说一个function类型对象就是一个函数对象。
还能作为lambda的返回方式
function<int(double)> f;
f = [](double x){ return x>0?x+0.5:x;}
所以其实可以发现,function对于回调、将操作作为参数传递等机制很有用。
template<typename...Var>
void algo(int s,Var ...v){
auto helper = [&s,&v...]{ return s*h1(v...)+h2(v...);}
}
这样都是引用捕获的方式。
再来看看[=,capture_list]
auto f = [=,&ir](){ return i;};
f();
cout << i << endl;
同样对于名字没有出现在捕获列表中的局部变量,通过值隐式捕获。捕获列表不允许包含this。列出的名字必须都是引用捕获(&前缀)。
可变,一般情况下,人们不希望修改函数对象的状态,因此我们可以看到调用运算符是设置成不可修改const的状态,只有极少数情况下才需要修改。所以不能因为少用我们就不学,我们还是的知道。mutable这叫可变
stable_sort(words,begin(),words.end(),[count]()mutable{ return --count;});
--count负责递减闭包中的v的副本。值捕获也能改,看怎么改。
[capture list](parameter list) -> return type { function body} // 这是标准的形式
我们继续来看上述的标准形式,后面有一个->尾置返回的类型,这是神马?这就是显式指定返回类型
int a;
auto z1 = [=,a]()->int{if(a)return 1;else return 2;};// 指定返回int
好了,他的使用形式到这里也就是差不多了,他高频使用于泛型算法,作为可调用对象,平常要很多行代码搞定的事情,它一句话可以搞定,而且这么写了之后执行效率反而提高了。因为编译器有可能使用”循环展开“来加速执行过程
以前不会lambda,你要这么写
vector<int> v;
v.push_back( 1 );
v.push_back( 2 );
//...
for ( auto itr = v.begin(), end = v.end(); itr != end; itr++ ){
cout << *itr;
}
现在会lambda,你可以这么写
vector<int> v;
v.push_back( 1 );
v.push_back( 2 );
...
for_each(v.begin(),v.end(),[](int val){
cout << val;
});
其实lambda表达式是什么呢?lambda是一种局部类类型,它含有一个构造函数以及一个const成员函数operator()()。
如上,一般情况下,可以使用function配合lambda表达式一起使用,若只是使用一下,起个名字,就可以用auto.
auto rev = [&](char *b,char *e){...};
或者如果lambda不捕获任何值,可以赋值给一个函数指针(强调,类型必须匹配)
double (*p1)(double) = [](double a){return sqrt(a);};
说完了,lambda,我们再说说bind,这个函数适配器。
使用bind()是有条件的,给定一个函数和一组实参,bind会生成一个可用该函数"剩余"实参
简单的🌰
double cube(double);
auto cube2 = bind(cube,2);
这是什么意思呢?cube2()会用实参2调用cube(),即cube(2)。
其实也不需要绑定所有的实参,说明了是函数适配器,可以有绑定某一个实参的情况,使用_n
,_n是占位符,_1表示实参在函数对象中应放在什么位置
using namespace placeholders;
void f(int,const string&);
auto g = bind(f,2,_1); // 将实参绑定到2
g("hello");// <=>f(2,"hello");
所以可以看到,bind,接受一个可调用对象,并生成一个新的可调用对象,来适应原对象的参数列表。
auto newCallable = bind(callable,arg_list);
经典用法,用bind修改参数的顺序
double f(int a,int b,int c,int d,int e);
auto g = bind(a,b,_2,c,_1);
g(1,2);
就会将1给了_1,2给了_2。达到了交换的效果。
详情可以参考我的另一篇文稿bind参数绑定
好了,可调用对象,大概就是这样,用于算法的计算,缩短代码,让代码的可读性提高。非常有用。下一篇将讲述省略符形参以及可变参数模版。