条款40:使仿函数类可适配

优质
小牛编辑
142浏览
2023-12-01

条款40:使仿函数类可适配

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

list<Widget*> widgetPtrs;
bool isInteresting(const Widget *pw);

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

list<Widget*>::iterator i = find_if(widgetPtrs.begin(), widgetPtrs.end(),
						isInteresting);
if (i != widgetPtrs.end()) {
	...							// 处理第一个
}								// 有趣的指向
								// Widget的指针

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

list<Widget*>::iterator i =
	find_if(widgetPtrs.begin(), widgetPtrs.end(),
			not1(isInteresting));			// 错误!不能编译

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

list<Widget*>::iterator i =
	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<typename T>
class MeetsThreshold: public std::unary_function<Widget, bool>{
private:
	const T threshold;

public:
	MeetsThreshold(const T& threshold);
	bool operator()(const Widget&) const;
	...
};

struct WidgetNameCompare:
	public std::binary_function<Widget, Widget, bool>{
	bool operator()(const Widget& lhs, const Widget& 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<T>、plus<T>等)一般写为struct。再看看WidgetNameCompare:

struct WidgetNameCompare:
	public std::binary_function<Widget, Widget, bool> {
	bool operator()(cost Widget& lhs, const Widget& rhs) const;
}

虽然operator的实参类型是const Widget&,但传给binary_function的是Widget。一般来说,传给unary_function或binary_function的非指针类型都去掉了const和引用。(不要问为什么。理由不很好也不很有趣。如果你真的想知道,写一些没有去掉它们的程序,然后去解剖编译器诊断结果。如果完成了这步,你仍然对这个问题感兴趣,访问boost.org(参见条款50)然后看看他们关于特性和函数对象适配器的工作。)

当operator()的参数是指针时这个规则变了。这里有一个和WidgetNameCompare相似的结构,但这个使用Widget*指针:

struct PtrWidgetNameCompare:
	public std::binary_function<const Widget*, const Widget*, bool> {
	bool operator()(const Widget* lhs, const Widget* rhs) const;
};

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

不要忘记所有使用这些unary_function和binary_function基类基本理由的冗繁的文字。这些类提供函数对象适配器需要的typedef,所以从那些类继承产生可适配的函数对象。那使我们这么做:

list<Widget> widgets;
...
list<Widget>::reverse_iterator i1 =				// 找到最后一个不
	find_if(widgets.rbegin(), widgets.rend(),		// 适合阈值10的widget
			not1(MeetsThreshold<int>(10)));	// (不管意味着什么)

Widget w(构造函数实参);
list<Widget>::iterator i2 =					// 找到第一个在由
	find_if(widgets.begin(), widgets.end(),		// WidgetNameCompare定义
			bind2nd(WidgetNameCompare(), w);	// 的排序顺序上先于w的widget

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

STL函数对象模仿了C++函数,而一个C++函数只有一套参数类型和一个返回类型。结果,STL暗中假设每个仿函数类只有一个operator()函数,而且这个函数的参数和返回类型要被传给unary_function或binary_function(与我们刚讨论过的引用和指针类型的规则一致)。这意味着,虽然可能很诱人,但你不能通过建立一个单独的含有两个operator()函数的struct试图组合WidgetNameCompare和PtrWidgetNameCompare的功能。如果你那么做了,这个仿函数可能可以和最多一种它的调用形式(你传参数给binary_function的那个)适配,而一个只能一半适配的仿函数可能只比完全不能适配要好。

有时候有必要给一个仿函数类多个调用形式(因此得放弃可适配性),条款7、20、23和25给了这种情况的例子。但是那种仿函数类是例外,不是规则。可适配性是重要的,每次你写仿函数类时都应该努力促进它。