Effective STL 条款39

类别:编程语言 点击:0 评论:0 推荐:
条款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:第三次被调用的时候。其他时候它返回假。

class BadPredicate: // 关于这个基类的更多信息 public unary_function<Widget, bool> { // 请参见条款40 public: BadPredicate(): timesCalled(0) {} // 把timesCalled初始化为0 bool operator()(const Widget&) { return ++timesCalled == 3; } private: size_t timesCalled; };

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

vector<Widget> vw; // 建立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 <typename FwdIterator, typename Predicate> FwdIterator remove_if(FwdIterator begin, FwdIterator end, Predicate p) { begin = find_if(begin, end, p); if (begin == end) return begin; else { FwdIterator next = begin; return remove_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。如果你这么做了,你的编译器不会让你改变任何类数据成员。

class BadPredicate: public unary_function<Widget, bool> { public: bool operator()(const Widget&) 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类产生的对象一样糟:

bool anotherBadPredicate(const Widget&, const Widget&) { static int timesCalled = 0; // 不!不!不!不!不!不!不! return ++timesCalled == 3; // 判断式应该是纯函数, } // 纯函数没有状态

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

本文地址:http://com.8s8s.com/it/it28971.htm