C++重载函数调用运算符,“行为像函数一样”

如果我们重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。因为这样的类同时也能存储状态,所以与普通函数相比他们更加灵活
举个简单的例子,下面这个名为absInt的struct含有一个调用运算符,该运算符负责返回其参数的绝对值

struct absInt{
	int operator()(int val)const {
		return val<0?-val:val;
	}
}

这个类只定义一种才做:函数调用运算符,它负责接受一个int类型的实参,然后返回该实参的绝对值。
我们使用调用运算符的另一个absInt对象作用于一个实参列表,这一过程看起来非常想调用函数的过程

int i=-42;
absInt=absObj;
int ui=absInt(j)

即使absInt是一个对象而非函数,我们也能“调用”该对象。调用该对象实际上是爱运行重载的调用运算符。
如果类定义了调用运算符,则该类的对象称为函数对象,因为可以调用这种对象,所以我们说这些对象的“行为像函数一样”

含有状态的对象类

和其他类一样,函数对象类除了operator()之外也可以包含其他成员函数。函数对象类通常含有一次额数据成员,这些恒源被用于定制调用运算符的操作
举个例子,我们将定义一个打印string实参内容的类。迷人情况下,我们的类会将内容写到cout中,每个string之间以空格隔开。同时也允许类的用户提供其他可写入的流及其他分隔符。我们将该类定义如下

class PrintString{
public:
	printString(ostream&o=cout,char c=''):os(o),sep(c){}
	void operator()(const string&s)const{os<<s<<sep;}
public:
	ostream &os;//用于写入的目的流
	char sep;//用于将不同输出隔开的字符
}

当定义printString对象时,对于分隔符及输出流既可以使用默认值也可以提供我们自己的值:

PrintString printer;
printString(s);
PrintString errors(cerror, '\n');
errors(s);

函数对象长产作为泛型算法的实参。例如,可以使用标准库for_each算法和我们自己的PrintString类来打印容器

for_each(vs.begin().vs.end(),PrintString(cerr,'/n'));

for_each的第三个实参是类型PrintString 的一个临时对象,其中我们用cerr和换行符初始化了改对象。当程序调用for_each时,会将vs中的每个元素一次打印到cerr中,元素之间换行符分隔

lambda是函数对象

当我们编写一个lambda后,编译器将表达式翻译成一个未命名类的未命名对象。在lambda表达式产生的类中含有一个重载的函数调用运算符,例如,对于我们传递给stable_sort作为最后一个实参的lambda表达式

//根据单词的长度对其进行排序,对于长度相同的单词按照字母表顺序排列
stable_sort(words.begin(),words.end(),[](const string &a,const string &b)
{return a.size()<b.size();})

其行为类似于下面这个类的一个命名对象

class ShorterString{
public:
	bool operator(const string &a,const string &b)const
	{return a.size()<b.size();}
}

用这个类来代替lambda表达式后,我们可以重写并调用stable_sort;

stable_sort(words.begin(),words.end(),ShorterString())

表示lambda及相应捕获行为的类

如我们所知,当一个lambda表达式通过引用捕获变量时,将由程序负责确保lambda执行时引用的对象确实存在。因此,编译器可以直接使用该引用而无需在lambda产生的类中将其存储未数据成员
相反,通过值捕获的变量被拷贝到lambda中。因此这种lambda产生的类必须为每个值的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。举个例子,找到第一个长度小于给定值的string对象

//获得第一个指向满足条件元素的迭代器,该元素满足size() is>=sz
auto wc=find_if(words.begin(),words.end(),[sz](const string &a){return a.size()>sz;})

该lambda表达式产生的类将形如

class SizeComp{
	SizeComp(size_t n):sz(n){}//该形参对应捕获的变量
	//该变量都熬用运算符的返回类型、形参和函数体斗鱼lambda一致
	bool operator()(const string &s){return s.size()>=sz;}
private:
	size_t sz;
}

和我们的ShorterString类不同,上面这个类含有一个数据成员以及一个用于用于初始化该成员的构造函数。这个合成的类不含有默认构造函数,因为要想使用这个类必须提供一个实参

//获得第一个指向满足条件的迭代器,该元素满足size() is >=sz
auto wc=find_if(words.begin(),words.end(),SizeComp(sz));

标准库定义的函数对象

标准库定义了一组表示算数运算符、关系运算符和逻辑运算符的类,每个类分别定义了一个执行命名操作的调用运算符。例如plus定义了一个函数调用运算符用于对一个运算符对象执行+操作;modulus类定于一了一个调用运算符执行二元的%操作;equal_to类执行==,等等。
这些类被定义成模板的形式,我们可以为其指定具体的应用类型,这里的调用即调用运算符的形参类型。例如,plus<string>令string加法运算符作用于string对象;plus<int>的运算对象是int;Plus<Sales_data>对Sales_data对象执行加法运算,以此类推

plus<int> intAdd;
negate<int> intNegate;
//使用intAdd::operator(int, int)
int sum=intAdd(10,20);
sum=intNegate(intAdd(10,20));
sum=intAdd(10,intNegate(10));
在算法中使用标准库函数对象

表示运算符的函数对象类常用来替换算法中的默认运算符。如我们所知,在默认情况下排列算法使用operator<将系列按照升序排列。如果要执行降序排列的化,我们传入一个greater类型的对象。该类将产生一个调用运算符并负责执行待排序类型的大于运算。例如,如果svec是一个vector<string>\

sort(svec.begin().svec.end().greater<string>());

特别需要注意的是,标准库规定其函数对象对于指针同样使用。我们之前曾今介绍过比较两个无关指针将产生定义的行为。然而我们可能会希望通过比较指针的内存地址来sort指针的vector。直接这么做将产生未定义的行为,因此我们可以使用一个标准库函数来实现该目的

可调用对象与function

c++语言C++语言中有几种可调用的对象:函数、函数指针、lambda表达式,bind创建的对象以及重载了函数调用运算符的类
和其他对象一样,可调用的对象也有类型。例如每个lambda有自己唯一的类类型;函数即函数指针的类型则由其返回类型和实参决定
然而两种不同类型的可调用对象却可能共享同一种调用形式。调用形式指明了调用返回的类型以及传递给调用的实参参数。一种调用形式对应一个函数类型例如

int add(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 add(int,int)

我们可能希望使用这些可调用对象构建一个简单的桌面计算器。为了实现这一目的,需要定义一个函数表用于存储指向这些可调用对象的“指针”。当程序需要执行某种特定的操作是,从表中查找该调用函数。
在C++语言中,函数表很容易通过map来实现。对于此例来说,我们使用一个表示运算符号的string对象作为关键字;实用实现运算符的函数作为值。当我们需要要求给定运算符的值,先通过运算符索引map,然后调用找到的那个函数。
假定我们的所有函数都相互独立,并且只处理关于int的二元运算,则map可以定义成如下的形式:

map<string,int(*)(int,int)>binops;

我们可以按照下面的形式将add的指针添加到binop中;

bimop.insert({"+",add});

但我们不能将mod或者divide存入binops

标准库function函数

我们可以使用一个名为function的新的标准库来解决上述问题,function定义在functional头文件中

  • function<T> f:f是一个用来存储可调用对象的空functional,这些可调用对象的调用形式应该于函数类型T相同
  • function<T>f(nullptr):显示地构造一个空function
  • function<T>f(obj):在f中存储对象obj的副本
  • f:将f作为条件;当f含有一个可调用对象时为真;否则为假
  • f(args):调用f的对象,参数为args
    定义为function<T>的成员类型
  • result_type:该function类型的可调用对象返回的类型
  • 当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){return i%j};

这个function类型我们可以宠幸定义map

map<string,functional<int(int,int)>> binops;

我们能把所有可调用对象,包括函数指针,lambda或者函数对象在内,都添加到这个map之中

map<string,functional<int(int,int)>> binops={
{'+',add},
{'-',std::minus<int>()},
{'/',divide()},
{'*',[](int i,int j){return i*j}},
{'%',mod}
}

function类型重载了调用运算符,该运算符接受自己的实参然后将其传递给存好的调用对象

binops["+"](10,5);
binops["-"](10,5);
binops["/"](10,5);
binops["*"](10,5);
binops["%"](10,5);
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值