函数对象-摘自《C++沉思录》Andrew Koenig

来源:互联网 发布:淘宝口红代购ysl 编辑:程序博客网 时间:2024/05/01 09:41

       函数对象提供了一种方法,将要调用的函数与准备传递给这个函数的隐式参数捆绑起来。这就允许我们使用相当简单的语法来建立复杂的表达式

       函数对象表示例一种操作。通过组合函数对象,我们可以得到复杂的操作。之所以能进行这种组合,是因为函数对象可以把函数当作值来处理,从而带来了很大的灵活性。

1.   一个例子

       标准库中一个叫 find_if 的函数,它的参数是一对迭代器和一个判断式(predicate)--一个生成布尔值 truth 的函数。find_if 函数返回由这对迭代器限定的范围内第一个使判断式得到真值的迭代器的值。

       假设 v 为类型为 vector<int>的对象,可以这样写:

bool greater1000(int n) {return n > 1000;}find_if(v.begin(), v.end(), greater1000);

       首先,考虑用一个叫做 greater 的模板类的库,我们可以这样重写 greater1000:

bool greater1000(int n) {greater<int> gt;return gt(n, 1000);}

       接着,标准库中有一个叫做 bind2nd 的函数配接器,假设已有函数对象 f,且 f 有两个参数其中一个值 v,则 bind2nd 就创建一个新的函数对象 g,g(x) 具有和 f(x, v)相同的值。取名为 bind2nd 是因为这个配接器绑定值 v 到第二个参数 f 上。于是可以重写 greater1000 为:

bool greater1000(int n) {greater<int> gt;return (bind2nd(gt, 1000)) (n);}

       也可以写成:

bool greater1000(int n) {return (bind2nd(greater<int>(), 1000)) (n);}

       更新 find_if 为:

find_if(v.begin(), v.end(), bind2nd(greater<int>(), 1000));

       假设我们要使例子更实用一些,在 v 中查找第一个大于 x 的值。那么 find_if 可以写成:

find_if(v.begin(), v.end(), bind2nd(greater<int>(), (x)));

       而 greater1000在这儿起不到什么作用,原因在于值 1000 是在 greater 的定义中被创建的。如果试图写一个 greater1000 的更通用的版本,就会遇到麻烦:

bool greater_x(int n) {return n > x;}

       x 的值在哪里设置呢?我们不能将 x 作为第二个参数传递,因为 find_if要求它的判断式只有一个参数。显然,下一步我们必须让 x 成为一个全局变量。真烦!

       函数对象配接器所解决的问题之一,是把信息从使用该函数对象(这里是 find_if)的部分通过程序的另一部分(这个部分对要传递的信息(此处信息指 find_if 的函数体)一无所知)传递到第三部分中(这里指与 bind2nd 有关的判断表达式),在第三部分中,信息将被取出来。

2.   函数指针

       在有些编程语言中,函数是“第一级值”(first-class value)。在这些语言中,可以将函数作为参数传递,并把它们当作值返回,还可把它们当作表达式的组件使用等。

       C++不属于这类语言,但这一点并不是显而易见的。因为对于C++程序而言,将函数作为参数传递,并把它们的地址存储在数据结构中是很常见的操作。例如,假设我们想对某个数组的所有元素都运用某个给定函数。如果知道这个函数有一个 int 参数,并且生成 void 类型,我们可以如下编写代码:

void apply(void f(int), int* p, int n) {for (int i = 0; i < n; i++)f(p[i]);}

       这是不是说明C++把函数也当作第一级值了呢?

       本例中的第一个隐蔽之处就是,f 虽然看上去像函数,其实根本就不是函数。相反,它是一个函数指针。和在 C 中一样,C++不可能有函数类型的变量,所以任何声明这种变量的企图都将立即被转换成指向函数的指针声明。和在 C 中一样,所有对函数指针的调用都等价于对这个指针所指向的函数的调用。所以,前面的例子就等价于

void apply(void (*fp)(int), int* p, int n) {for (int i = 0; i < n; i++)(*fp)(p[i]);}

       那又怎么样?函数和函数指针之间有什么重大差异吗?这个差异和任何指针与其所指向的对象之间的差异是类似的:不可能通过操纵指针创建这样的对象C++函数的总存储空间在程序执行之前就固定了。一旦程序开始运行,就无法创建新函数了。为了理解为什么说不能动态创建新函数是个问题,我们来思考一下如何写一个C++函数,以便把两个函数组合起来成为第三个函数。组合是我们所能想到的创建新函数的最简单的方法之一。现在为了简单起见,我们将假设每个函数都有一个整数参数并返回一个整数结果。然后,假设我们有一对函数 f 和 g:
       extern int f(int);

       extern int g(int);

       我们希望能够使用下面的语句:

       int (*h)(int) = compose(f, g);

       具有一种特性,就是对于任何整数 n 而言,h(n)(你会回想起来,它等价于 (*h)(n))将会等于 f(g(n))  。

       C++没有提供直接做这件事的方法。我们可以杜撰出如下的代码:

//这段代码无效int (*compose(int f(int), int g(int)))(int x) {int result(int n) { return f(g(n)); }return result;}

       这里,compose 视图用两个函数 f 和 g 来定义一个函数,当应用于 x 时可以得到 f(g(x)) 的函数;但是由于两个原因它不可能成功。第一个原因就是C++不支持嵌套函数,这就意味着 result 的定义是非法的。而且由于 result 需要在块作用域之内访问 f 和 g,所以没有简便的方法可以绕过这个限制。也就是说,我们不能简单的使 result 全局化:

//这段代码也不起作用int result(int n) { return f(g(n)); }int (*compose(int f(int), int g(int)))(int x) {return result;}

       这个例子的问题出在 f 和 g 在 result 中没有定义。
       第二个问题更难以捉摸,假设C++允许嵌套函数--毕竟有些C++实现把它当作一种扩展,那么这样做会成功吗?

       可惜的是,答案是“实际上不会成功”。为了了解原因,我们稍微修改了一下 compose 函数:

//这段代码还是不起作用int (*compose(int f(int), int g(int)))(int x) {int (*fp)(int) = f;int (*gp)(int) = g;int result(int n) { return fp(gp(n)); }return result;}

       其中所做的改变是要将 f 和 g 的地址复制到两个局部变量 fp 和 gp 中去。现在,假设我们调用 compose,它返回一个指向 result 的指针。因为 fp 和 gp 是 compose 的局部变量,所以一旦 compose 返回它们就消失了。如果我们现在调用 result,它将试图使用这些局部变量,但是这些变量已经被删除了。结果很可能导致程序运行崩溃。

       通过检查上面 compose 的最后一个版本,我们应该很容易明白这个程序失败的原因。然而,第一个版本中也存在着相同的问题。唯一不同的是第一个版本中的 f 和 g 不是普通的局部变量,而是形参。这个区别无关大局:当 compose 返回时它们也消失;也就是说但 result 试图访问它们时也会导致崩溃。

       那么,显然编写 compose 函数除了需要常规的基于堆栈的实现外,还需要某种自动内存管理。实际上,把函数当作第一级值处理的语言通常也支持垃圾回收机制。尽管C++将垃圾收集作为语言的标准部分会给很多方面带来好处,但是存在太多的困难使我们不能这样定义C++。有没有办法来回避这种局限,并能以一种更通用的方式将函数作为值处理呢?


3.   函数对象

       困难在于我们的 compose 函数想要创建新函数,而C++不允许直接这么做。无论何时面对这样一个问题时,我们都应该考虑用一个类对象来表示这种解决方案。如果 compose 不能返回一个函数,那么它可能返回一个行为和函数类似的类对象

       这样的对象叫做函数对象通常,函数对象是某种类类型的对象,该类类型包括一个 operator() 成员函数。有了这个成员函数就可以把类对象当作函数来使用。那么,举例来说,如果我们写这样一个类:

class F {public:int operator() (int);// ...}

则类 F 对象的行为将在某种程度上类似于那些采用整数参数并返回整数结果的函数。例如,在

int main() {F f;int n = f(42);}

中,f(42)等价于 f.operator()(42)。也就是说,f(42)获得对象 f,用参数 42 调用它的 operator() 成员函数。

       我们可以把这些技巧作为运用类的基础来组合函数:

class Intcomp {public:Intcomp(int (*f0)(int), int (*g0)(int)): fp(f0), gp(g0) {}int operator() (int n) const {return (*fp)((*gp)(n));}private:int (*fp)(int);int (*gp)(int);};

       这里,构造函数准备记住函数指针 f0 和 g0,operator()(int) 完成这两个函数的组合。首先,operator()传递它的 int 参数 n 给由 gp 指向的函数,然后把 gp 返回的结果传给 fp。所以,如果已有和前面一样的函数 f 和 g,我们就能使用 Intcomp 来将它们组合起来:

extern int f(int);extern int g(int);int main() {Intcomp fg(f, g);fg(42);// 等价于f(g(42))}

       这项技术起码在原理上解决了组合问题,因为每个 Intcomp 都专门开辟出位置来存放被组合函数的标识。然而由于它只能组合函数,却不能组合函数对象,所以仍然不是一个实用的解决方案。这样一来,譬如说我们不能用 Intcomp 来联合一个具有任何特性的 Intcomp。换句话说,尽管可以使用 Intcomp 类来联合两个函数,但不能用它来联合多于两个的函数。对此我们能做些什么改进呢?


4.   函数对象模板

       我们似乎可以创建一个类,其对象不仅可以用来组合函数,而且还可以用来组合函数对象。在C++中定义一个这样的类的常见做法是采用我们称之为 Comp 的模板。模板类 Comp 将有一些模板参数,其中包括两个将被组合在一起的事物的类型。为以后的运用着想,我们还将再给 Comp 增加两个类型参数。当我们调用 Comp 对象时,我们会给它一个类型值,而它则会返回另一个类型的值。从而,我们就把这两种类型作为 Comp 模板的两个新增加的类型参数。通过这种方法,我们再也不会受到只能处理返回 int 的函数的限制了:

template<class F, class G, class X, class Y> class Comp {public:Comp(F f0, G g0): f(f0), g(g0) {}Y operator() (X x) const {return f(g(x));}private:F f;G g;};

       这里的指导思想是类型 Comp<F, G, X, Y>的对象能够将类型为 F 的函数(或者函数对象)与另一个类型为 G 的函数(或者函数对象)组合起来,得到一个参数类型为 X,结果类型为 Y 的对象。除此之外,细节都与类 Intcomp 几乎相同。可以用类 Comp 来联合前面的整数函数 f 和 g:

int main() {Comp<int (*)(int), int (*)(int), int, int> fg(f, g);fg(42);// 调用f(g(42))}

       这样做可以凑效,但是需要两次规定函数类型 int(*)(int),实在不值得欣赏。实际上,如果我们想组合函数 f 和 g,则 fg 的完全类型必须作为第一个模板参数。这将使得类型变得令人望而生畏:

Comp<Comp<int (*)(int), int (*)(int), int, int>, int (*)(int), int, int> fgf(fg, f);

       有没有将它简化到实用程度的方法呢?


5.   隐藏中间类型

       让我们想想要实现的目标。现在,我们已经有一种可以联合两个函数或者两个函数对象的方法,但表示联合的函数对象的类型太复杂。我们希望能够写这样的语句:

       Composition fg(f, g);       // 过于乐观

       当这只能是奢望。原因在于,当我们稍后相求 fg(42) 的值时,编译器不知道表达式应该采用哪些类型。无论 fg(42) 的类型是什么,都必须隐含在 fg 的类型中,而且要和  fg 所接受的参数的类型相似。所以,我们最多只能这样写:

       Composition<int, int> fg(f, g);

       这里,第一个int是函数对象 fg 接受的参数的类型,第二个 int 是它被调用时返回结果的类型。有了这个定义,起码就不难写出部分该类的部分定义了:

template<class X, class Y> class Composition {public:// ...Y operator() (X) const;// ...};

       但是我们怎样才能实现这个类呢?这种情况下,构造函数应该怎样编写呢?

       构造函数提出了一个有趣的问题,因为构造函数 Composition 必须能够接受函数和函数对象的任何组合--尤其是 Composition。这就意味着构造函数必须是一个模板,以适应所有这些可能情况

template<class X, class Y> class Composition {public:template<class F, class G> Composition(F, G);Y operator() (X) const;// ...};

       但是这样做回避了问题。类型 F 和 G 不属于类 Composition 的类型,因为它们在这里不是模板参数。然而,类 Composition 也许可以通过保存 Comp<F, G, X, Y>的对象来发挥作用。如果不把 F 或者 G 作为类 Comp 本身的模板参数,就很难做到这一点。所幸的是,C++提供了一种叫做继承的机制。

6.   一种类型包罗万象

       假设我们重写 Comp,使 Comp<F, G, X, Y>继承自某个不依赖于 F 或者 G 的其他类。我们称这个类为 Comp_basc<X, Y>。那么,在类 Composition 中,我们就可以保存一个指向 Comp_base<X, Y> 的指针。

       从 Comp_base 开始着手,可能是由内向外解开这个死结的好方法。从概念上讲,Comp_base<X, Y> 对象可能表示的是一个接受 X 参数并返回 Y 结果的任意函数对象。因此,我们将给它提供一个虚函数 operator(),因为所有继承自 Comp_base<X, Y> 的类中的 operator() 都接受一个相同类型(叫做X)的参数以及返回一个相同类型(叫做Y)的结果。由于我们不希望特地为一个普通的 Comp_base 定义 operator(),所以将它创建成纯虚函数。另外,由于涉及到继承,所以 Comp_base 还需要一个虚析构函数。

       作一下预测的话,我们会清楚目标是要能够复制 Composition 对象。复制 Composition 对象涉及到要在不知道确切类型的情况下复制某种 Comp 类型的对象。所以,我们需要在 Composition 中有一个纯虚函数来复制派生类对象。

       具备了所有的这些前提,就可以得到下面这个基类:

template<class X, class Y> class Comp_base {public:virtual Y operator()(X) const = 0;virtual Comp_base* clone() const = 0;virtual ~Comp_base() {}};

       现在,我们用 Comp_base 作为基类来重写类 Comp,并且为 Comp 增加一个适当的 clone 函数,该函数将覆盖 Comp_base 中的纯虚函数:

template<class F, class G, class X, class Y> class Comp: public Comp_base<X, Y> {public:Comp(F f0, G g0): f(f0), g(g0) {}Y operator() (X x) const { return f(g(x)); }Comp_base<X, Y>* clone() const {return new Comp(*this);}private:F f;G g;};

       这样,我们可以令 Composition 类包含一个指向 Comp_base 的指针:

template<class X, class Y> class Composition {public:template<class F, class G> Composition(F, G);Y operator() (X) const;private:Comp_base<X, Y>* p;// ...};

       无论类何时获得一个指针类型的成员,我们都应该考虑复制该类对象的时候如何处理这个指针。这种情况下,我们希望复制底层对象,按照第5章讨论的那样将类 Composition 作为一个代理--从根本上说,这就是我们预先在类 Comp_base 中添加 clone 函数的原因。因此,我们必须在类 Composition 中写个显式的复制构造函数和析构函数:

template<class X, class Y> class Composition {public:template<class F, class G> Composition(F, G);Composition(const Composition &);Composition & operator=(const Composition &);~Composition();Y operator() (X) const;private:Comp_base<X, Y>* p;};

 

7.   实现

       至此,实现类 Composition 应该是相当简单的事情了。在用一对类型为 F 和 G 的对象构造 Composition<X, Y>时,我们将创建一个 Comp<F, G, X, Y>对象,并把它的地址存储到指针 p 中。下面看上去有点奇怪的语法就是定义一个模板类的模板成员的方法。

       针对 Composition<X, Y>的每个变量,它都定义了构造函数 Composition<F, G>,所以构造函数的全称就是:

Composition<X, Y>::Composition<F, G>(F, G)

       我们这样定义这个构造函数:

template<class X, class Y> template<class F, class G> Composition<X, Y>::Composition(F f, G g):p(new Comp<F, G, X, Y> (f, g)) {}

      这个构造函数用一个指向 Comp<F, G, X, Y>的指针初始化了类型为 Comp_base<X, Y>的成员 p。由于类 Comp<F, G, X, Y>继承自类 Comp_base<X, Y>,所以这个初始化是有效的。

       析构函数只删除 p 所指向的对象:

template<class X, class Y> Composition<X, Y>::~Composition() {delete p;}


       复制构造函数和赋值操作符利用了类 Comp_base 中的纯虚函数 clone:

template<class X, class Y> Composition::Composition(const Composition & c): p(c.p->clone()) {}template<class X, class Y> Composition & operator=(const Composition & c) {if (this != &c) {delete p;p = c.p->clone();}return *this;}

       最后,operator() 运用类 Comp_base 中的虚 operator():

template<class X, class Y> Y Composition::operator() (X x) const {return (*p) (x);// p->operator() (X)}

       到这一步了我们希望可以这样写:

extern int f(int);extern int g(int);extern int h(int);int main() {Composition<int, int> fg(f, g);Composition<int, int> fgh(fg, h);}

并且希望 fg 和 fgh 是同一类型,尽管它们所做的工作互不相同。


8.   讨论

       研究这个例子的意义之一,让大家体会到我们必须做大量的工作才能绕过一个看似简单的语言局限。另一点就是这个例子说明了扩展语言以使之允许函数组合,决不像看上去的那么简单。另外,如果我们已经一次性定义了这些函数对象,以后就可以直接使用它们了。一旦理解了这些概念,我们就能够用清晰简洁的形式来使用它们,而跟这个错综复杂的形式挥手道别。

       例如,有一个叫做 transform 的标准库函数,它对序列中每个元素都运用函数或者函数对象,从而获得一个新的序列。如果 a 是一个有 100 个元素的数组,那么

       transform(a, a+100, a, f);

将依次对 a 的每个元素运用 f,并将结果存回到 a 的对应元素中。

       假若我们希望用 transform 使数组的每个元素都加上一个整数 n 呢?那么,我们可以定义一个加整数的函数对象:

class Add_an_integer {public:Add_an_integer(int n0): n(n0) {}operator() const (int x) { return x + n; }private:int n;}

然后应该调用

       transform(a, a + 100, a, Add_an_integer(n));

为了这个目的而定义一个独立的函数对象是很麻烦的。

       实际上,我们可以做得更好。标准库提供了一种叫做函数配接器的模板,我们可以在联合的过程中使用它们来定义类似 Add_an_integer 的类,而不必去编写类的定义。









 

0 0
原创粉丝点击