分类: C/C++
2011-08-02 21:09:36
出处:%E2d_%B7%B3_%DE%B2%C2%D2/blog/item/46a982f9c813a85d252df280.html
与其它许多 Boost 库一样,这个库完全定义在头文件中,这意味着你不必构建任何东西就可以开始使用。但是,知道一点关于 lambda 表达式的东西肯定是有帮助的。接下来的章节会带你浏览一下这个库,还包括如何在 lambda 表达式中进行异常处理!这个库非常广泛,前面还有很多强大的东西。一个 lambda 表达式通常也称为匿名函数(unnamed function)。它在需要的时 候进行声明和定义,即就地进行。这非常有用,因为我们常常需要在一个算法中定义另一个算法,这是语言本身所不能支持的。作为替代,我们通过从更大的范围引 进函数和函数对象来具体定义行为,或者使用嵌套的循环结构,把算法表达式写入循环中。我们将看到,这正是 lambda 表达式可以发挥的地方。本节内有许多例子,通常例子的一部分是示范如何用"传统"的编码方法来解决问题。这样做的目的是,看看 lambda 表达式在何时以及如何帮助程序写出更具逻辑且更少的代码。使用 lambda 表达式的确存在一定的学习曲线,而且它的语法初看起来有点可怕。就象每种新的范式或工具,它们都需要去学习,但是请相信我,得到的好处肯定超过付出的代 价。
一个简单的开始第一个使用 Boost.Lambda 的程序将会提升你对 lambda 表达式的喜爱。首先,请注意 lambda 类型是声明在 boost::lambda 名字空间中,你需要用一个 using 指示符或 using 声明来把这些 lambda 声明带入你的作用域。包含头文件 "boost/lambda/lambda.hpp" 就可以使用这个库的主要功能了,对于我们第一个程序这样已经足够了。
第一个表达式看起来很奇特,你可以在脑子里按着括号来划分这个表达式;第一部分就是一个 lambda 表达式,它的意思基本上是说,"打印这些参数到 std::cout, 但不是立即就做,因为我还不知道这三个参数"。表达式的第二部分才是真正调用这个函数,它说,"嘿!这里有你要的三个参数"。我们再来看看这个表达式的第一部分。
你会注意到表达式中有三个占位符,命名为 _1, _2, 和 _3 [1]。 这些占位符为 lambda 表达式指出了延后的参数。注意,跟许多函数式编程语言的语法不一样,创建 lambda 表达式时没有关键字或名字;占位符的出现表明了这是一个 lambda 表达式。所以,这是一个接受三个参数的 lambda 表达式,参数的类型可以是任何支持 operator<< 流操作的类型。参数按 1-3-2 的顺序打印到 cout 。在这个例子中,我们把这个表达式用括号括起来,然后调用得到的这个函数对象,传递三个参数给它:"Hello", "friend", 和 "my". 输出的结果如下:
[1]你可能没想到象 _1 这样的标识符是合法的,但它们的确是。标识符不能由数字打头,但可以由下划线打头,而数字可以出现在标识符的其它任何地方。
Hello my friend!通常,我们要把函数对象传入算法,这是我们要进一步研究的,但是我们来试验一些更有用的东西,把 lambda 表达式存入另一个延后调用的函数,名为 boost::function. 这个有用的发明将在下一章 "Library 11: Function 11" 中讨论,现在你只有知道可以传递一个函数或函数对象给 boost::function 的实例并保存它以备后用就可以了。在本例中,我们定义了这样的一个函数 f,象这样:
boost::function这个声明表示 f 可以存放用三个参数调用的函数和函数对象,参数的类型全部为 int. 然后,我们用一个 lambda 表达式把一个函数对象赋给它,这个表达式表示了算法 X=S*T+U, 并且把这个算式及其结果打印到 cout.
boost::function如你所见,在一个表达式中,占位符可以多次使用。我们的函数 f 现在可以象一个普通函数那样调用了,如下:
f(1,2,3); f(3,2,1);运行这段代码的输出如下。
1*2+3=5 3*2+1=7任意使用标准操作符(操作符还可以被重载!)的表达式都可以用在 lambda 表达式中,并可以保存下来以后调用,或者直接传递给某个算法。你要留意,当一个 lambda 表达式没有使用占位符时(我们还没有看到如何实现,但的确可以这样用),那么结果将是一个无参函数(对象)。作为对比,只使用 _1 时,结果是一个单参数函数对象;只使用 _1 和 _2 时,结果则是一个二元函数对象;当只使用 _1, _2, 和 _3 时,结果就是一个三元函数对象。这第一个 lambda 表达式受益于这样一个事实,即该表达式只使用了内建或常用的C++操作符,这样就可以直接编写算法。接下来,我们看看如何绑定表达式到其它函数、类成员函数,甚至是数据成员!
在操作符不够用时就用绑定到目前为止,我们已经看到如果有操作符可以支持我们的表达式,一切顺利,但并不总是如此的。有时我们需要把调用另一个函数作为表达式的一部分,这通常要借助于绑定;这种绑定与我们前面在创建 lambda 表达式时见过的绑定有所不同,它需要一个单独的关键字,bind (嘿,这真是个聪明的名字!)。一个绑定表达式就是一个被延迟的函数调用,可以是普通函数或成员函数。该函数可以有零个或多个参数,某些参数可以直接设定,另一些则可以在函数调用时给出。对于当前版本的 Boost.Lambda, 最多可支持九个参数(其中三个可以通过使用占位符在稍后给出)。要使用绑定器,你需要包含头文件"boost/lambda/bind.hpp"。
在绑定到一个函数时,第一个参数就是该函数的地址,后面的参数则是函数的参数。对于一个非静态成员函数,总是有一个隐式的 this 参数;在一个 bind 表达式中,必须显式地加上 this 参数。为方便起见,不论对象是通过引用传递或是通过指针传递,语法都是一样的。因此,在绑定到一个成员函数时,第二个参数(即函数指针后的第一个)就是将要调用该函数的真实对象。绑定到数据成员也是可以的,下面的例子也将有所示范:
这个例子开始时先创建一个 std::map ,键类型为 int 且值类型为 std::string 。记住,std::map 的 value_type 是一个由键类型和值类型组成的 std::pair 。因此,对于我们的 map, value_type 就是 std::pair
这个表达式生成一个函数对象,它被调用时将取出它的参数,即我们前面讨论的 pair 中的嵌套类型 value_type 的数据成员 first。在我们的例子中,first 是 map 的键类型,它是一个 const int. 对于成员函数,语法是完全相同的。但你要留意,我们的 lambda 表达式多做了一点;表达式的第一部分是
std::cout << "key=" << ...它可以编译,也可以工作,但它可能不能达到目的。这个表达式不是一个 lambda 表达式;它只是一个表达式而已,再没有别的了。执行时,它打印 key=, 当这个表达式被求值时它仅执行一次,而不是对于每个被 std::for_each 所访问的元素执行一次。在这个例子中,原意是把 key= 作为我们的每一个 keys_and_values 键/值对的前缀。在早一点的那些例子中,我们也是这样写的,但那里没有出现这些问题。原因在于,那里我们用了一个占位符来作为 operator<< 的第一个参数,这样就使得它成为一个有效的 lambda 表达式。而这里,我们必须告诉 Boost.Lambda 要创建一个包含 "key=" 的函数对象。这就要使用函数 constant, 它创建一个无参函数对象,即不带参数的函数对象;它仅仅保存其参数,然后在被调用时返回它。
std::cout << constant("key=") << ...这个小小的修改使得所有输出都不一样了,以下是该程序的运行输出结果。
What's wrong with the following expression? key=0, value=Nothing, if you ask me 3, value=Less than pi 42, value=You tell me ...and why does this work as expected? key=0, value=Nothing, if you ask me key=3, value=Less than pi key=42, value=You tell me keys_and_values.size()=3 keys_and_values.max_size()=4294967295例子的最后一部分是一个绑定到成员函数的绑定器,而不是绑定到数据成员;语法是一样的,而且你可以看 到在这两种情形下,都不需要显式地表明函数的返回类型。这种奇妙的事情是由于函数或成员函数的返回类型可以被自动推断,如果是绑定到数据成员,其类型同样 可以自动得到。但是,有一种情形不能得到返回类型,即当被绑定的是函数对象时;对于普通函数和成员函数,推断其返回类型是一件简单的事情[2],但对于函数对象则不可能。有两种方法绕过这个语言的限制,第一种是由 Lambda 库自己来解决:通过显式地给出 bind 的模板参数来替代返回类型推断,如下所示。
[2] 你也得小心行事。我们只是说它在技术上可行。
有两种版本的方法来关闭返回类型推断系统,短格式的版本只需把返回类型作为模板参数传给 bind, 另一个版本则使用 ret, 它要括住不能进行自动推断的 lambda/bind 表达式。在嵌套的 lambda 表达式中,这很容易会就得乏味,不过还有一种更好的方法可以让推断成功。我们将在本章稍后进行介绍。
请注意,一个绑定表达式可以由另一个绑定表达式组成,这使得绑定器成为了进行函数组合的强大工具。嵌套的绑定有许多强大的功能,但是要小心使用,因为这些强大的功能同时也带来了读写以及理解代码上的额外的复杂性。
我不喜欢 _1, _2, and _3,我可以用别的名字吗?有的人对预定义的占位符名称不满意,因此本库提供了简便的方法来把它们[3]改为任意用户想用的名字。这是通过声明一些类型为 boost::lambda::placeholderX_type 的变量来实现的,其中 X 为 1, 2, 或 3. 例如,假设某人喜欢用 Arg1, Arg2, 和 Arg3 来作占位符的名字:
[3] 技术上,是增加新的名字。
你定义的占位符变量可以象 _1, _2, 和 _3 一样使用。另外请注意这里的函数 for_all ,它提供了一个简便的方法,当你经常要对一个容器中的所有元素进行操作时,可以比用 for_each 减少一些键击次数。这个函数接受两个参数:一个容器的引用,以及一个函数或函数对象。该容器中的每个元素将被提供给这个函数或函数对象。我认为它有时会非常有用,也许你也这样认为。运行这个程序将产生以下输出:
What are the names of the placeholders? Arg1, Arg2, and Arg3!创建你自己的占位符可能会影响其它阅读你的代码的人;多数知道 Boost.Lambda (或 Boost.Bind) 的程序员都熟悉占位符名称 _1, _2, 和 _3. 如果你决定把它们称为 q, w, 和 e, 你就需要解释给你的同事听它们有什么意思。(而且你可能要经常重复地进行解释!)
我想给我的常量和变量命名!有时,给常量和变量命名可以提高代码的可读性。你也记得,我们有时需要创建一个不是立即求值的 lambda 表达式。这时可以使用 constant 或 var; 它们分别对应于常量或变量。我们已经用过 constant 了,基本上 var 也是相同的用法。对于复杂或长一些的 lambda 表达式,对一个或多个常量给出名字可以使得表达式更易于理解;对于变量也是如此。要创建命名的常量和变量,你只需要定义一个类型为 boost::lambda::constant_type
总是使用 constant 会很让人讨厌。下面是一个例子,它命名了两个常量,newline 和 space,并把它们用于 lambda 表达式。
这是一个避免重复键入的好方法,也可以使 lambda 表达式更清楚些。下面是一个类似的例子,首先定义一个类型 memorizer, 用于跟踪曾经赋给它的所有值。然后,用 var_type 创建一个命名变量,用于后面的 lambda 表达式。你将会看到命名常量要比命名变量更常用到,但也有些情形会需要使用命名变量[4]。
[4] 特别是使用 lambda 循环结构时。
这就是它的全部了,但在你认为自己已经明白了所有东西之前,先回答这个问题:在以下声明下 T 应该是什么类型?
constant_type它是一个 char*? 一个 const char*? 都不是,它的正确类型是一个含有六个字符(还有一个结束用的空字符)的数组的常量引用,所以我们应该这样写:
constant_type这很不好看,而且对于需要修改这个字符串的人来说也很痛苦,所以我更愿意使用 std::string 来写。
constant_type这次,你需要比上一次多敲几个字,但你不需要再计算字符的个数,如果你要改变这个字符串,也没有问题。
ptr_fun 和 mem_fun 到哪去了?也许你还在怀念它们,由于 Boost.Lambda 创建了与标准一致的函数对象,所以没有必要再记住这些标准库中的适配器类型了。一个绑定了函数或成员函数的 lambda 表达式可以很好地工作,而且不论绑定的是什么类型,其语法都是一致的。这可以让代码更注重其任务而不是某些奇特的语法。以下例子说明了这些好处:
这里真的不需要用 lambda 表达式吗?相对于使用三个不同的结构来完成同一件事情,我们可以只需向 bind 指出要干什么,然后它就会去做。在这个例子中,需要用 std::bind1st 来把 some_class 的实例绑定到调用中;而对于 Boost.Lambda,这是它工作的一部分。因此,下次你再想是否要用 ptr_fun, mem_fun, 或 mem_fun_ref 时,停下来,使用 Boost.Lambda 来代替它!
无须我们常常要按顺序对一些元素执行算术操作,而标准库提供了多个函数对象来执行算术操作,如 plus, minus, divides, modulus, 等等。但是,这些函数对象需要我们多打很多字,而且常常需要绑定一个参数,这时应该使用绑定器。如果要嵌套这些算术操作,表达式很快就会变得难以使用,而 这正是 lambda 表达式可以发挥巨大作用的地方。因为我们正在处理的是操作符,既是算术上的也是C++术语上的,所以我们有能力使用 lambda 表达式直接编写我们的算法代码。作为一个小的动机,考虑一个简单的问题,对一个数值增加4。然后再考虑另一个问题,完成与标准库算法(如 transform)同样的工作。虽然第一个问题非常自然,而第二个则完全不一样(它需要你手工写循环)。但使用 lambda 表达式,只需关注算法本身。在下例中,我们先使用 std::bind1st 和 std::plus 对容器中的每个元素加4,然后我们使用 lambda 来减4。
差别是令人惊讶的!在使用"传统"方法进行加4时,对于未经训练的眼睛来说,很难看出究竟在干什么。从代码中我们看到,我们将一个缺省构造的 std::plus 实例的第一个参数绑定到4。而 lambda 表达式则写成从元素减4。如果你认为使用 bind1st 和 plus 的版本还不坏,你可以试试更长的表达式。