可调用对象与function
C++语言中有几种可调用的对象:函数、函数指针、lambda 表达式、bind创建的对象以及重载了函数调用运算符的类.
和其他对象一样,可调用的对象也有类型。例如,每个lambda 有它自己唯一的(未命名)类类型;函数及函数指针的类型则由其返回值类型和实参类型决定,等等。
然而,两个不同类型的可调用对象却可能共享同一种调用形式。
调用形式指明了调用返回的类型以及传递给调用的实参类型。
一种调用形式对应一个函数类型,例如:
int(int, int)
是一个函数类型,它接受两个int、返回一个int。
不同类型可能具有相同的调用形式
对于几个可调用对象共享同一种调用形式的情况,有时我们会希望把它们看成具有相同的类型。
例如,考虑下列不同类型的可调用对象:
//普通函数
int add(int i, int j) { return i + j;}
// lambda,其产生一个未命名的函数对象类
auto mod = [](int i, int j) { return i % j; };
//函数对象类
struct divide
{
int operator() (int denominator, int divisor){
return denominator / divisor;
}
};
上面这些可调用对象分别对其参数执行了不同的算术运算,尽管它们的类型各不相同,但是共享同一种调用形式:
int(int,int)
我们可能希望使用这些可调用对象构建一个简单的桌面计算器。
为了实现这一目的,需要定义一个函数表(function table)用于存储指向这些可调用对象的“指针”。当程序需要执行某个特定的操作时,从表中查找该调用的函数。
在C++语言中,函数表很容易通过map 来实现。
对于此例来说,我们使用一个表示运算符符号的string对象作为关键字;使用实现运算符的函数作为值。当我们需要求给定运算符的值时,先通过运算符索引map,然后调用找到的那个元素。
假定我们的所有函数都相互独立,并且只处理关于int 的二元运算,则map可以定义成如下的形式:
// 构建从运算符到函数指针的映射关系,其中函数接受两个int、返回一个int
map<string, int(*)(int,int)> binops;
我们可以按照下面的形式将ada的指针添加到binops中.
//正确:add是一个指向正确类型函数的指针
binops.insert(("+", add));
//("+",add}是一个pair
但是我们不能将mod或者divide存入binops:
binops.insert(("%",mod));
//错误:mod不是一个函数指针
问题在于mod是个lambda表达式,而每个lambda 有它自己的类类型,该类型与存储在binops中的值的类型不匹配。
标准库function类型
我们可以使用一个名为function 的新的标准库类型解决上述问题,function定义在functional头文件中,表14.3列举出了function定义的操作。
functioncT> f | f是一个用来存储可调用对象的空function,这些可调用对象的调用形式应该与函数类型T相同(即T是retType(args)) |
function<T> f(nullptr); | 显式地构造一个空function |
function<T> f(obj); | 在f中存储可调用对象。obj的副本 |
f | 将f作为条件:当含有一个可调用对象时为真;否则为假 |
f (args) | 调用f中的对象,参数是args |
result_type | 该 function类型的可调用对象返回的类型 |
argument _type first_argument_type second argument _type | 当T有一个或两个实参时定义的类型。如果只有一个实参,则argument_type 是该类型的同义词;如果有两个实参,则 first_argument_type 和 second argument_type分别代表两个实参的类型 |
function 是一个模板,和我们使用过的其他模板一样,当创建一个具体的function 类型时我们必须提供额外的信息。
在此例中,所谓额外的信息是指该function 类型能够表示的对象的调用形式。参考其他模板,我们在一对尖括号内指定类型:
function<int(int, int)>
在这里我们声明了一个function类型,它可以表示接受两个int、返回一个int的可调用对象。
因此,我们可以用这个新声明的类型表示任意一种桌面计算器用到的类型;
function<int(int, int)> f1 = add;//函数指针
function<int(int, int)> f2 = divide();//函数对象类的对象
function<int(int, int)> f3=[](int i, int j){cout << f1(4,2) << endl; return i*j;};
//lambda
cout << f1(4,2)<<endl;//打印6
cout << f2(4,2)<< endl;//打印2
cout << f3(4,2) << endl;//打印8
使用这个 function类型我们可以重新定义map:
//列举了可调用对象与二元运算符对应关系的表格
//所有可调用对象都必须接受两个int、返回一个int
// 其中的元素可以是函数指针、函数对象或者 lambda
map<string, function<int(int, int)>> binops;
我们能把所有可调用对象,包括函数指针、lambda或者函数对象在内,都添加到这个map
map<string, function<int(int,int)>>binops={
{"+",add}, // 函数指针
{"-",std::minus<int>()}, // 标准库函数对象
{"/",divide()}, //用户定义的函数对象
{"*",[](int i,int j){return i*j;}},//未命名的lambda
{"%",mod} }; //命名了的lambda对象
我们的map中包含5个元素,尽管其中的可调用对象的类型各不相同,我们仍然能够把所有这些类型都存储在同一个function<int (int,int)>类型中。
一如往常,当我们索引map时将得到关联值的一个引用。如果我们索引binops,将得到function对象的引用。
function类型重载了调用运算符,该运算符接受它自己的实参然后将其传递给存好的可调用对象:
binops["+"](10,5);//调用add(10,5)
binops["-"](10,5);// 使用minus<int>对象的调用运算符
binops["/"](10,5);// 使用divide对象的调用运算符
binops["*"](10,5);// 调用lambda 函数对象
binops["%"](10,5);//调用lambda函数对象
我们依次调用了binops中存储的每个操作。在第一个调用中,我们获得的元素存放着一个指向add函数的指针,因此调用binops["+"](10,5)实际上是使用该指针调用add,并传入10和5。
在接下来的调用中,binops["-"]返回一个存放着std::minus<int>类型对象的function,我们将执行该对象的调用运算符。
重载的函数与function
我们不能(直接)将重载函数的名字存入function类型的对象中:
int add(int i, int j) { return i + j; )
Sales_data add(const Sales _data&,const Sales_data&);
map<string, function<int(int, int)>> binops;
binops.insert(("+",add});
// 错误:哪个 add?
解决上述二义性问题的一条途径是存储函数指针而非函数的名字:
int (*fp)(int,int) = add;//指针所指的add是接受两个int的版本
binops.insert(("+",fp));// 正确:fp指向一个正确的add版本
同样,我们也能使用lambda 来消除二义性:
// 正确:使用 lambda 来指定我们希望使用的add版本
binops.insert( ("+",[](int a, int b) (return add(a, b)i)));
lambda 内部的函数调用传入了两个int,因此该调用只能匹配接受两个int的add版本,而这也正是执行lambda时真正调用的函数。
新版本标准库中的 function 类与旧版本中的 unary_function和binary_function没有关联,后两个类已经被更通用的bind函数替代了。