目录
C++语言中可调用的对象
- 函数
- 函数指针
- lambda表达式
- bind创建的对象
- 重载了函数调用运算符的类
和其他对象一样,可调用的对象也有类型。例如,每个lambda有它自己唯一的(未命名)类类型;函数及函数指针的类型则由其返回值类型和实参类型决定,等等。
然而,两个不同类型的可调用对象却可能共享一种调用形式(call signature)。调用形式指明了调用返回的类型以及传递给调用的实参类型。一种调用形式对应一个函数类型,例如:
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
来实现。
//构建从运算符到函数指针的映射关系,其中函数接受两个int、返回一个int
map<string, int(*)(int, int)> binops;
我们可以按照下面的形式将add
的指针添加到binops
中:
//正确:add是一个指向正确类型函数的指针
binops.insert({"+", add}); //{"+", add}是一个pair
但是我们不能将mod
或者divide
存入binops
:
binops.insert({"%", mod}); //错误:mod不是一个函数指针
问题在于mod
是个lambda
表达式,而每个lambda
有它自己的类类型,该类型与存储在binops
中的值的类型不匹配。
标准库function类型
我们可以使用一个名为function
的新的标准库类型解决上述问题,function
定义在functional
头文件中。
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) { 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);}});
lambda
内部的函数调用传入了两个int
,因此该调用只能匹配接受两个int
版本的add
版本,而这也正是执行lambda
时真正调用的函数。