20.1 C++高级话题与新标准-函数调用运算符与function类模板
20.2 C++高级话题与新标准-万能引用
20.3 C++高级话题与新标准-理解函数模板类型推断与查看类型推断结果
20.4 C++高级话题与新标准-引用折叠、转发、完美转发与forward
20.5 C++高级话题与新标准-理解auto类型推断与auto应用场合
20.6 C++高级话题与新标准-详解decltype含义与decltype主要用途
20.7 C++高级话题与新标准-可调用对象、std::function与std::bind
20.8 C++高级话题与新标准-lambda表达式与for_each、find_if简介
20.9 C++高级话题与新标准-lambda表达式捕获模式的陷阱分析和展示
20.10 C++高级话题与新标准-可变参数函数、initializer_list与省略号形参
20.11 C++高级话题与新标准-萃取技术概念与范例等
1.函数调用运算符与function类模板
1.1 学习C++的体会
(1)就是对语言本身的学习,语言本身是基础,所以,这一个学习环节对于任何一个人来讲都绕不过去,是C++程序员的必经之路。
(2)就是大量练习。笔者这本书提供了极多的讲解范例,读者不但要看,在有条件的情况下尽可能亲自动手去实践、去练习,这非常非常重要,熟能生巧,缺少练习会导致缺少对自己的自信心和底气,会使自己对C++的学习感到挫折。
(3)就是当自身具备了一定的能力后就要开始阅读优秀的人写的优秀代码,对于C++开发方向,优秀的人、优秀的开源代码很多很多,吸收这些代码的优点和精华,为自己所用,这非常关键。但是写这些代码的人往往有各自不同的习惯和擅长,所以,要想读懂这些代码需要对C++基础知识掌握的比较广泛,不然读代码的时候会感觉磕磕绊绊,非常影响速度,也打击自己的信心。
本章内容会包含很多知识点,这些知识点很可能会决定读者将来能否读懂其他人写的代码,所以本章内容和其他章节同等重要甚至更加重要,读者切不可因为这是最后面的章节而掉以轻心,疏于学习。
1.2 函数调用运算符
写一个函数与调用一个函数都很容易,例如,写一个函数:
void func(int i)
{
cout << "这是函数func()" << i << endl;
return;
}
func(5);
可以注意到,调用一个函数很简单,首先是函数名,后面跟一对圆括号,如果这个函数有参数,就在函数调用位置的函数名后圆括号中给出实参,在函数定义位置函数名后圆括号中给出形参。
通过上面的代码可以感受到,那就是函数调用总离不开一对圆括号,没错,"()“就是函数调用的一个明显标记,这个”()“有一个称呼叫函数调用运算符,请记住这个名字。
那么如果在类中重载了这个函数调用运算符"()",就可以像使用函数一样使用该类的对象,或者换句话说就可以像函数调用一样来"调用"该类的对象。
在cpp的前面位置写一个类,重载这个函数调用运算符”()"看一下:
class biggerthanzero
{
public:
//重载函数调用运算符()
int operator()(int value) const //如果值<0就返回0,否则返回实际的值
{
if (value < 0) return 0;
return value;
}
};
观察上面这个函数调用运算符,它接受一个int类型参数,如果该参数<0则返回0,否则返回实际值。也就是说,这个函数调用运算符的功能是:不会返回比0小的值,仅此而已。
写出来这个函数调用运算符后,怎样使用呢?分两步使用即可:
(1)定义一个该类的对象。
(2)像函数调用一样使用该对象,也就是在"()"中增加实参列表。
在main主函数中,加入如下代码:
{
int i = 200;
biggerthanzero obj; //含有函数调用运算符的对象
int result = obj(i); //调用类中重载的函数调用运算符(),本行代码等价于:int result = obj.operator()(i);
cout << result << endl; //200
}
现在往类biggerthanzero中增加一个public修饰的带一个参数的构造函数。如下:
class biggerthanzero
{
public:
biggerthanzero(int i)
{
cout << "biggerthanzero::biggerthanzero(int i)构造函数执行了" << endl;
}
}
那么,现在main主函数中的代码需要改造,否则编译报错。看看改造后的main主函数中的代码,注意与刚刚所写的main主函数中的代码相互比较:
{
int i = 200;
biggerthanzero obj(1); //这是对象定义并初始化,所以调用的是biggerthanzero构造函数
obj(i); //这个才是调用类中重载的函数调用运算符()
}
这个写法读者刚接触可能会不适应,因为obj本身是一个类对象,但是obj(i)这种写法是属于函数调用的写法。以往学习的知识都是调用一个函数,这里变成了调用一个对象。当然现在就明白了,这实际上是调用这个类中重载的"()“运算符,并且在上面的范例中,重载的”()"运算符还带了一个参数。
所以得到一个结论:只要这个对象所属的类重载了"()“,那么这个类对象就变成了可调用对象(函数对象),而且可以调用多个版本的”()",只要在参数类型或数量上有差别即可。现在可以把类biggerthanzero中的构造函数先注释掉了。
1.3 不同调用对象的相同调用形式
int echovalue(int value)
{
cout << value << endl;
return value;
}
上面这个echovalue函数的功能是输出接收到的形参值并将该值原样返回。
现在观察一下echovalue函数以及刚刚写的biggerthanzero类中重载的函数调用运算符,发现它们的形参和返回值是相同的,这就叫作“调用形式相同”,注意这个概念。
有一句话叫“一种调用形式对应一个函数类型”,不管是否理解这句话,先放在这里。
本来echovalue函数与类biggerthanzero对象之间没有什么关系,但是因为它们调用形式相同,从而扯到了“函数类型相同”这个关系上来了。这就好像你明明跟某个人不认识,但是左拐右拐,你发现他居然是你姑姑的亲戚的连桥,这跟你就搭上关系了。
再看“一种调用形式对应一个函数类型”这句话,上面范例中“函数类型”是什么?函数类型应该如下:
int(int)
上面这行代表一个“函数类型”:接收一个int参数,返回一个int值。
现在笔者引入“可调用对象”这个概念,那么如下两个都是可调用对象:
· echovalue函数。
· 重载了函数调用运算符"()"的biggerthanzero类所生成的对象。
现在希望把这些可调用对象的指针保存起来,保存起来的目的是方便后续随时调用这些“可调用对象”,这些指针其实就是在C语言部分学习过的函数指针。
int (*p) (int x, int y);
p = max;
int c = (*p)(5,19);
要保存这些可调用的对象的指针,这里通过一个map可以实现。map是一个C++标准库中的容器,类似vector,只不过vector里每个元素都是一个数据(例如是一个int或者是一个string类型数据),而map里的每个元素都是两个数据,分别称为“键”和“值”,如果这个map容器里有多个元素,那么多个元素的“键”不允许重复,“值”可以重复。所以在map容器中可以通过一个键寻找一个值。map也是一个比较常用的容器,读者可以通过搜索引擎进一步了解和熟悉。
在这里可以用一个字符串做“键”,用上面这些“可调用对象的指针”做值。请注意看笔者写程序来构建这个map。首先在cpp的前面包含如下头文件:
#include <map>
map<string, int(*)(int)> myoper;
上面这行定义了一个map容器,“键”是一个字符串,“值”是一个函数指针,但是这里放在map中时,函数指针只保留了“*”,指针名就去掉了,读者要注意这种写法,把这种写法记住。这样就可以通过“键”来查找值,这些“值”都是可调用对象的指针(指向这些可调用对象),那么就能够实现对这些可调用对象的调用。
继续在main主函数中增加代码:
myoper.insert({ "ev",echovalue });
上面这行代码就把echovalue函数指针(函数名代表函数首地址,所以可以认为是一个函数指针)放到map容器中来了。接下来,这个来自于类的函数对象怎么放到map容器中呢?在这里用类名用对象名都不行,例如如下代码都报错:
{
biggerthanzero obj; //含有函数调用运算符的对象
myoper.insert({ "bt",biggerthanzero }); //报错
myoper.insert({ "bt",obj }); //报错
}
这说明系统并没有把类biggerthanzero的对象obj看成一个函数指针,当然系统更不可能把类biggerthanzero看成一个函数指针(因为这是个类名嘛)。
1.4 标准库function类型简介
C++11中,标准库里有一个叫作function的类模板,这个类模板是用来包装一个可调用对象的。要使用这个类模板,在.cpp前面要包含如下头文件:
#include <functional>
function<int(int)>
上面这种写法是一个类类型(类名就是function<int(int)>),用来代表(包装)一个可调用对象,它所代表的这个可调用对象接收一个int参数,返回一个int值。
所以看一看用法,用这个新声明的类型来代表上面的这些类型就都没问题。
这里直接写几行代码看看这个function<int(int)>类型怎么用:
{
biggerthanzero obj; //含有函数调用运算符的对象
function<int(int)> f1 = echovalue; //函数指针
function<int(int)> f2 = obj; //类对象,可以,因为类中重载函数调用运算符()
function<int(int)> f3 = biggerthanzero();//用类名加()生成一个对象 ,可以,因为类中重载函数调用运算符()
//调用一下
f1(5); //5
cout << f2(3) << endl; //3
cout << f3(-5) << endl; //0
}
现在这个function<int(int)>类型就好像一个标准一样,函数指针也好,函数对象也罢,都包装成function<int(int)>这种类型的对象,相当于把类型给统一了。
所以要改造一下myoper容器里每一项键值对中“值”这一项的格式。修改后的容器应该如下:
map<string, function<int(int)> > myoper;
{
biggerthanzero obj;
map<string, function<int(int)> > myoper = {
{ "ev",echovalue},
{ "bt",obj },
{ "bt2",biggerthanzero() }
};
}
所以读者看到了,可以把函数、可调用对象等都包装成function<int(int)>对象。
继续看一看这个map里面的可调用对象应该如何来调用:
{
myoper["ev"](12); //"ev"是键,那myoper["ev"]就代表值,其实也就是这个echovalue函数,现在就调用这个函数了
cout << myoper["bt"](3) << endl; //调用obj对象的函数调用运算符()
cout << myoper["bt2"](-5) << endl; //调用biggerthanzero类对象的函数调用运算符()
}
通过对function类模板的研究,发现一个很尴尬的问题,刚才用如下语句成功地把函数echovalue通过function包装起来了:
function<int(int)> f1 = echovalue;
但如果有一个重载的echovalue函数,参数和返回值都不一样,例如:
void echovalue()
{
return;
}
此时语句行function<int(int)>f1=echovalue;编译时就会报错。这说明一个问题:只要这个函数是重载的,那么就无法包装进function中。这也是一个二义性导致的问题,那么这个问题怎样解决?可以通过定义一个函数指针来解决。看代码:
{
int(*fp)(int) = echovalue; //定义个函数指针,不会产生二义性,因为函数指针里有对应的参数类型和返回类型
function<int(int)> f1 = fp; //直接塞进去函数指针而不是函数名
}
1.5 总结
本节所讲的内容说复杂也复杂说简单也简单,读者第一次碰到这些程序写法会感觉不习惯、别扭、奇怪等,但实际细想一想,熟悉一下这些代码,发现其实并不复杂。