仿函数解读.docx

上传人:b****9 文档编号:25579841 上传时间:2023-06-09 格式:DOCX 页数:21 大小:29.34KB
下载 相关 举报
仿函数解读.docx_第1页
第1页 / 共21页
仿函数解读.docx_第2页
第2页 / 共21页
仿函数解读.docx_第3页
第3页 / 共21页
仿函数解读.docx_第4页
第4页 / 共21页
仿函数解读.docx_第5页
第5页 / 共21页
点击查看更多>>
下载资源
资源描述

仿函数解读.docx

《仿函数解读.docx》由会员分享,可在线阅读,更多相关《仿函数解读.docx(21页珍藏版)》请在冰豆网上搜索。

仿函数解读.docx

仿函数解读

仿函数,仿函数类,函数等等

条款38:

把仿函数类设计为用于值传递

C和C++都不允许你真的把函数作为参数传递给其他函数。

取而代之的是,你必须传指针给函数。

比如,这里有一个标准库函数qsort的声明:

voidqsort(void*base,size_tnmemb,size_tsize,

int(*cmpfcn)(constvoid*,constvoid*));

条款46解释了为什么sort算法一般来说是比qsort函数更好的选择,但在这里那不是问题。

问题是qsort声明的参数cmpfcn。

一旦你忽略了所有的星号,就可以清楚地看出作为cmpfcn传递的参数,一个指向函数的指针,是从调用端拷贝(也就是,值传递)给qsort。

这是C和C++标准库都遵循的一般准则,也就是,函数指针是值传递。

STL函数对象在函数指针之后成型,所以STL中的习惯是当传给函数和从函数返回时函数对象也是值传递的(也就是拷贝)。

最好的证据是标准的for_each声明,这个算法通过值传递获取和返回函数对象:

template

classFunction>

Function//注意值返回

for_each(InputIteratorfirst,

InputIteratorlast,

Functionf);//注意值传递

实际上,值传递的情况并不是完全打不破的,因为for_each的调用者在调用点可以显式指定参数类型。

比如,下面的代码可以使for_each通过引用传递和返回它的仿函数:

classDoSomething:

publicunary_function{//条款40解释了这个基类

voidoperator()(intx){...}

...

};

typedefdeque:

:

iteratorDequeIntIter;//方便的typedef

dequedi;

...

DoSomethingd;//建立一个函数对象

...

for_each

DoSomething&>(di.begin(),//类型是DequeIntIter

di.end(),//和DoSomething&;

d);//这迫使d按引用

//传递和返回

但是STL的用户不能做这样的事,如果函数对象是引用传递,有些STL算法的实现甚至不能编译。

在本条款的剩余部分,我会继续假设函数对象总是值传递。

实际上,这事实上总是真的。

因为函数对象以值传递和返回,你的任务就是确保当那么传递(也就是拷贝)时你的函数对象行为良好。

这暗示了两个东西。

第一,你的函数对象应该很小。

否则它们的拷贝会很昂贵。

第二,你的函数对象必须单态(也就是,非多态)——它们不能用虚函数。

那是因为派生类对象以值传递代入基类类型的参数会造成切割问题:

在拷贝时,它们的派生部分被删除。

(切割问题怎么影响你使用STL的另一个例子参见条款3。

当然效率很重要,避免切割问题也是,但不是所有的仿函数都是小的、单态的。

函数对象比真的函数优越的的原因之一是仿函数可以包含你需要的所有状态。

有些函数对象自然会很重,保持传这样的仿函数给STL算法和传它们的函数版本一样容易是很重要的。

禁止多态仿函数是不切实际的。

C++支持继承层次和动态绑定,这些特性在设计仿函数类和其他东西的时候一样有用。

仿函数类如果缺少继承就像C++缺少“++”。

的确有办法让大的和/或多态的函数对象仍然允许它们把以值传递仿函数的方式遍布STL。

这就是了。

带着你要放进你的仿函数类的数据和/或多态,把它们移到另一个类中。

然后给你的仿函数一个指向这个新类的指针。

比如,如果你想要建立一个包含很多数据的多态仿函数类。

template

classBPFC:

//BPFC=“BigPolymorphic

public//FunctorClass”

unary_function{//条款40解释了这个基类

private:

Widgetw;//这个类有很多数据,

Intx;//所以用值传递

...//会影响效率

public:

virtualvoidoperator()(constT&val)const;//这是一个虚函数,

...//所以切割时会出问题

};

建立一个包含一个指向实现类的指针的小而单态的类,然后把所有数据和虚函数放到实现类:

template//用于修改的BPFC

classBPFCImpl{//的新实现类

private:

Widgetw;//以前在BPFC里的所有数据

intx;//现在在这里

...

virtual~BPFCImpl();//多态类需要

//虚析构函数

virtualvoidoperator()(constT&val)const;

friendclassBPFC;//让BPFC可以访问这些数据

};

template

classBPFC:

//小的,单态版的BPFC

publicunary_function{

private:

BPFCImpl*pImpl;//这是BPFC唯一的数据

public:

voidoperator()(constT&val)const//现在非虚;

{//调用BPFCImpl的

pImpl->operator()(val);

}

...

};

BPFC:

:

operator()的实现例证了BPFC所有的虚函数是怎么实现的:

它们调用了在BPFCImpl中它们真的虚函数。

结果是仿函数类(BPFC)是小而单态的,但可以访问大量状态而且行为多态。

我在这里忽略了很多细节,因为我勾勒出的基本技术在C++圈子中已经广为人知了。

《EffectiveC++》的条款34中有。

在Gamma等的《设计模式》[6]中,这叫做“Bridge模式”。

Sutter在他的《ExceptionalC++》[8]中叫它“Pimpl惯用法”.

从STL的视角看来,要记住的最重要的东西是使用这种技术的仿函数类必须支持合理方式的拷贝。

如果你是上面BPFC的作者,你就必须保证它的拷贝构造函数对指向的BPFCImpl对象做了合理的事情。

也许最简单的合理的东西是引用计数,使用类似Boost的shared_ptr,你可以在条款50中了解它.

实际上,对于本条款的目的,唯一你必须担心的是BPFC的拷贝构造函数的行为,因为当在STL中被传递或从一个函数返回时,函数对象总是被拷贝——值传递,记得吗?

那意味着两件事。

让它们小,而且让它们单态。

条款39:

用纯函数做判断式

我讨厌为你做这些,但我们必须从一个简短的词汇课开始:

∙判断式(predicate)是返回bool(或者其他可以隐式转化为bool的东西)。

判断式在STL中广泛使用。

标准关联容器的比较函数是判断式,判断式函数常常作为参数传递给算法,比如find_if和多种排序算法。

(排序算法的概览可以在条款31找到。

∙纯函数是返回值只依赖于参数的函数。

如果f是一个纯函数,x和y是对象,f(x,y)的返回值仅当x或y的值改变的时候才会改变。

在C++中,由纯函数引用的所有数据不是作为参数传进的就是在函数生存期内是常量。

(一般,这样的常量应该声明为const。

)如果一个纯函数引用的数据在不同次调用中可能改变,在不同的时候用同样的参数调用这个函数可能导致不同的结果,那就与纯函数的定义相反。

现在已经很清楚用纯函数作判断式是什么意思了。

我要做的所有事情就是使你相信我的建议是有根据的。

要帮我完成这件事,我希望你能原谅我再增加一个术语所给你带来的负担。

∙一个判断式类是一个仿函数类,它的operator()函数是一个判断式,也就是,它的operator()返回true或false。

正如你可以预料到的,任何STL想要一个判断式的地方,它都会接受一个真的判断式或一个判断式类对象。

就这些了,我保证!

现在我们已经准备好学习为什么这个条款提供了有遵循价值的指引。

条款38解释了函数对象是传值,所以你应该设计可以拷贝的函数对象。

用于判断式的函数对象,有另一个理由设计当它们拷贝时行为良好。

算法可能拷贝仿函数,在使用前暂时保存它们,而且有些算法实现利用了这个自由。

这个论点的一个重要结果是判断式函数必须是纯函数。

想知道这是为什么,先让我们假设你想要违反这个约束。

考虑下面(坏的实现)的判断式类。

不管传递的是什么参数,它严格地只返回一次true:

第三次被调用的时候。

其他时候它返回假。

classBadPredicate:

//关于这个基类的更多信息

publicunary_function{//请参见条款40

public:

BadPredicate():

timesCalled(0){}//把timesCalled初始化为0

booloperator()(constWidget&)

{

return++timesCalled==3;

}

private:

size_ttimesCalled;

};

假设我们用这个类来从一个vector中除去第三个Widget:

vectorvw;//建立vector,然后

//放一些Widgets进去

vw.erase(remove_if(vw.begin(),//去掉第三个Widget;

vw.end(),//关于erase和remove_if的关系

BadPredicate()),//请参见条款32

vw.end());

这段代码看起来很合理,但对于很多STL实现,它不仅会从vw中除去第三个元素,它也会除去第六个!

要知道这是怎么发生的,就该看看remove_if一般是怎么实现的。

记住remove_if不是一定要这么实现:

template

FwdIteratorremove_if(FwdIteratorbegin,FwdIteratorend,Predicatep)

{

begin=find_if(begin,end,p);

if(begin==end)returnbegin;

else{

FwdIteratornext=begin;

returnremove_copy_if(++next,end.begin,p);

}

}

这段代码的细节不重要,但注意判断式p先传给find_if,后传给remove_copy_if。

当然,在两种情况中,p是传值——是拷贝——到那些算法中的。

(技术上说,这不需要是真的,但实际上,是真的。

详细资料请参考条款38。

最初调用remove_if(用户代码中要从vw中除去第三个元素的那次调用)建立一个匿名BadPredicate对象,它把内部的timesCalled成员清零。

这个对象(在remove_if内部叫做p)然后被拷贝到find_if,所以find_if也接收了一个timesCalled等于0的BadPredicate对象。

find_if“调用”那个对象直到它返回true,所以调用了三次,find_if然后返回控制权到remove_if。

remove_if继续运行后面的调用remove_copy_if,传p的另一个拷贝作为一个判断式。

但p的timesCalled成员仍然是0!

find_if没有调用p,它调用的只是p的拷贝。

结果,第三次remove_copy_if调用它的判断式,它也将会返回true。

这就是为什么remove_if最终会从vw中删除两个Widgets而不是一个。

最简单的使你自己不摔跟头而进入语言陷阱的方法是在判断式类中把你的operator()函数声明为const。

如果你这么做了,你的编译器不会让你改变任何类数据成员。

classBadPredicate:

publicunary_function{

public:

booloperator()(constWidget&)const

{

return++timesCalled==3;//错误!

在const成员函数中

}//不能改变局部数据

};

因为这是避免我们刚测试过的问题的一个直截了当的方法,我几乎可以把本条款的题目改为“在判断式类中使operator()成为const”。

但那走得不够远。

甚至const成员函数可以访问multable数据成员、非const局部静态对象、非const类静态对象、名字空间域的非const对象和非const全局对象。

一个设计良好的判断式类也保证它的operator()函数独立于任何那类对象。

在判断式类中把operator()声明为const对于正确的行为来说是必要的,但不够充分。

一个行为良好的operator()当然是const,但不只如此。

它也得是一个纯函数。

本条款的前面,我强调了任何STL想要一个判断式的地方,它都会接受一个真的判断式或一个判断式类对象。

它在两个方向上都是对的。

在STL任何可以接受一个判断式类对象的地方,一个判断式函数(可能由ptr_fun改变——参见条款41)也是受欢迎的。

你现在明白判断式类中的operator()函数应该是纯函数,所以这个限制也扩展到判断式函数。

作为一个判断式,这个函数和从BadPredicate类产生的对象一样糟:

boolanotherBadPredicate(constWidget&,constWidget&)

{

staticinttimesCalled=0;//不!

不!

不!

不!

不!

不!

不!

return++timesCalled==3;//判断式应该是纯函数,

}//纯函数没有状态

不管你怎么写你的判断式,它们都应该是纯函数。

条款40:

使仿函数类可适配

假设我有一个Widget*指针的list和一个函数来决定这样的指针是否确定一个有趣的Widget:

listwidgetPtrs;

boolisInteresting(constWidget*pw);

如果我要在list中找第一个指向有趣的Widget的指针,这很简单:

list:

:

iteratori=find_if(widgetPtrs.begin(),widgetPtrs.end(),

isInteresting);

if(i!

=widgetPtrs.end()){

...//处理第一个

}//有趣的指向

//Widget的指针

但如果我想要找第一个指向不有趣的Widget的指针,显而易见的方法却编译失败:

list:

:

iteratori=

find_if(widgetPtrs.begin(),widgetPtrs.end(),

not1(isInteresting));//错误!

不能编译

取而代之的是,我必须对isInteresting应用ptr_fun在应用not1之前:

list:

:

iteratori=

find_if(widgetPtrs.begin(),widgetPtrs.end(),

not1(ptr_func(isInteresting)));//没问题

if(i!

=widgetPtrs.end()){

...//处理第一个

}//指向Widget的指针

那会引出一些问题。

为什么我必须在应用not1前对isInteresting应用ptr_fun?

ptr_fun为我做了什么,怎么完成上面的工作的?

答案多少有些令人惊讶。

ptr_fun做的唯一的事是使一些typedef有效。

就是这样。

not1需要这些typedef,这就是为什么可以把not1应用于ptr_fun,但不能直接对isInteresting应用not1。

因为是低级的函数指针,isInteresting缺乏not1需要的typedef。

not1不是STL中唯一有那些要求的组件。

四个标准函数适配器(not1、not2、bind1st和bind2nd)都需要存在某些typedef,一些其他人写的非标准STL兼容的适配器(比如来自SGI和Boost的——参见条款50)也需要。

提供这些必要的typedef的函数对象称为可适配的,而缺乏那些typedef的函数对象不可适配。

可适配的比不可适配的函数对象可以用于更多的场景,所以只要能做到你就应该使你的函数对象可适配。

这不花费你任何东西,而它可以为你仿函数类的客户购买一个便利的世界。

我知道,我知道。

我在卖弄,经常提及“某些typedef”而没有告诉你是什么。

问题中的typedef是argument_type、first_argument_type、second_argument_type和result_type,但不是那么直截了当,因为不同类型仿函数类需要提供那些名字的不同子集。

总的来说,除非你在写你自己的适配器(本书没有覆盖的主题),你才不需要知道任何关于那些typedef的事情。

那是因为提供它们的正规方法是从一个基类,或,更精确地说,一个基结构,继承它们。

operator()带一个参数的仿函数类,要继承的结构是std:

:

unary_function。

operator()带有两个参数的仿函数类,要继承的结构是std:

:

binary_function。

好,简单来说,unary_function和binary_function是模板,所以你不能直接继承它们。

取而代之的是,你必须从它们产生的类继承,而那就需要你指定一些类型参数。

对于unary_function,你必须指定的是由你的仿函数类的operator()所带的参数的类型和它的返回类型。

对于binary_function,你要指定三个类型:

你的operator的第一个和第二个参数的类型,和你的operator地返回类型。

这里有两个例子:

template

classMeetsThreshold:

publicstd:

:

unary_function{

private:

constTthreshold;

public:

MeetsThreshold(constT&threshold);

booloperator()(constWidget&)const;

...

};

structWidgetNameCompare:

std:

:

binary_function{

booloperator()(constWidget&lhs,constWidget&rhs)const;

};

在两种情况下,注意传给unary_function或binary_function的类型与传给仿函数类的operator()和从那儿返回的一样,虽然operator的返回类型作为最后一个参数被传递给unary_function或binary_function有一点古怪。

你可能注意到了MeetsThreshold是一个类,而WidgetNameCompare是一个结构。

MeetsThreshold有内部状态(它的阈值数据成员),而类是封装那些信息的合理方法。

WidgetNameCompare没有状态,因此不需要任何private的东西。

所有东西都是public的仿函数类的作者经常把它们声明为struct而不是class,也许只因为可以避免在基类和operator()函数前面输入“public”。

把这样的仿函数声明为class还是struct纯粹是一个个人风格问题。

如果你仍然在精炼你的个人风格,想找一些仿效的对象,看看无状态STL自己的仿函数类(比如,less、plus等)一般写为struct。

再看看WidgetNameCompare:

structWidgetNameCompare:

std:

:

binary_function{

booloperator()(costWidget&lhs,constWidget&rhs)const;

}

虽然operator的参数类型是constWidget&,但传给binary_function的是Widget。

一般来说,传给unary_function或binary_function的非指针类型都去掉了const和引用。

(不要问为什么。

理由不很好也不很有趣。

如果你真的想知道,写一些没有去掉它们的程序,然后去解剖编译器诊断结果。

如果完成了这步,你仍然对这个问题感兴趣,访问boost.org(参见条款50)然后看看他们关于特性(trait)和函数对象适配器的工作。

当operator()的参数是指针时这个规则变了。

这里有一个和WidgetNameCompare相似的结构,但这个使用Widget*指针:

structPtrWidgetNameCompare:

std:

:

binary_function{

booloperator()(constWidget*lhs,constWidget"rhs)const;

};

在这里,传给binary_function的类型和operator()所带的类型一样。

用于带有或返回指针的仿函数的一般规则是传给unary_function或binary_function的类型是operator()带有或返回的类型。

不要忘记所有使用这些unary_function和binary_function基类基本理由的冗繁的文字。

这些类提供函数对象适配器需要的typedef,所以从那些类继承产生可适配的函数对象。

那使我们这么做:

listwidgets;

...

list:

:

reverse_iteratori1=//找到最后一个不

find_if(widgets.rbegin(),widgets.rend(),//适合阈值10的widget

not1(MeetsThreshold(10)));//(不管意味着什么)

Widgetw(构造函数参数);

list:

:

iteratori2=//找到第一个在由

find_if(widgets.begin(),widgets.end(),//WidgetNameCompare定义

bind2nd(WidgetNameCompare(),w);//的排序顺序上先于w的widget

如果我们没有把仿函数类继承自unary_function或binary_function,这些例子都不能编译,因为not1和bind2nd都只和可适配的函数对象合作。

STL函

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 幼儿教育 > 育儿理论经验

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1