C++“准”标准库Boost学习指南(10):Boost.Lambda

Boost.Lambda

Lambda为C++提供lambda表达式及无名函数。在使用标准库算法时特别好用,Lambda允许函数在呼叫点创建,避免了创建多个小的函数对象。使用lambdas意味着更少的代码,在哪需要就在哪写,这比分散在代码各处的函数对象更清晰、更好维护。

Lambda 库如何改进你的程序?
  • 对函数和函数对象进行适配,使之可用于标准库算法
  • 绑定参数到函数调用
  • 将任意的表达式转换为可以兼容标准库算法的函数对象
  • 就地定义匿名函数,提高代码的可读性和可维护性
  • 在需要的时间和地点实现谓词

在使用标准库或其它采用相似设计的库时,需要依靠函数或函数对象来对算法进行配置,你通常要编写很多小的函数对象来执行一些非常简单的操作。就象我们在 " Library 9: Bind 9" 看到的那样,这很容易成为一个问题,因为有大量的小类分散在代码中,这样很难进行维护。另外,理解函数对象被调用处的代码会很难,因为有一部分的功能被定义在别的地方。一个好的解决办法是,想办法就在调用的地方定义这些函数或函数对象。这通常可以使代码写得更快,也更容易维护,因为函数的定义就在它被使用的地方。这正是 Boost.Lambda 库所要提供的,就地定义匿名函数。Boost.Lambda 可以创建直接定义和调用的函数对象,或者把它保存起来晚一些再调用。这与 Boost.Bind 库所提供的很相似,但 Boost.Lambda 除了可以进行参数绑定,还有其它功能,增加了控制结构、表达式到函数对象的自动转换,还支持在 lambda 表达式中的异常处理。

术语 lambda 表达式或 lambda 函数,来源于函数式编程与 lambda 演算。一个 lambda 抽象概念定义了一个匿名函数。虽然 lambda 抽象概念在函数式编程语言(functional programming language)中非常普遍,但是在象C++这样的命令式编程语言(imperative programming language)中则不是。但是,通过使用象表达式模板这样的先进技术,C++ 可以在语言中增加某种形式的 lambda 表达式。

创建 Lambda 库最早的动机是,可以在标准库算法中使用匿名函数。因为从1998年第一个C++标准发布后,标准库的使用非常广泛,我们对于什么好什么不好的认识快速增长,而其中一个存在疑问的就是,对于众多小函数对象的定义,好象只需要一个简单的表达式就可以满足了。显然这个库就是定位于解决这些函数对象的问题,但是对于 lambda 函数的使用还有很大的探索空间。现在,lambda 函数已经可以使用了,我们可以把它应用于以前需要用完全不同的方法来解决的问题。令人着迷和兴奋的是,象C++这样一种成熟的语言还可以探索出新的编程技术。匿名函数和表达式模板的出现会带来怎样的新用法和新方法呢?事实是,我们不知道,因为我们还没有全力去试验它!尽管如此,这里所关注的是这个库明确要解决的实际问题,即通过就地定义 lambda 表达式和函数来避免代码膨胀和功能的分散。我们可以用它做出更多惊人的事情,有了它我们可以更为简洁,这可以同时满足程序员和他们的经理,前者可以更加集中精力在手边的问题,后者可以获得更高生产效率的好处(希望也是更容易维护的代码!)。

Lambda 如何适用于标准库?

这个库用于解决一个使用标准库算法时常会遇见的问题,即需要为了满足算法的要求而定义很多简单的函数对象。几乎所有的标准库算法都有一个接受函数对象的版本,这个函数对象用于执行如排序、等同性检验、转换等操作。标准库通过绑定器 bind1st 和 bind2nd 支持有限的函数组合。但是,它们能做的事情非常有限,它们只能提供参数绑定,而不能绑定表达式。在 Boost.Lambda 库中,既有对绑定参数的灵活支持,也可以直接从表达式创建函数对象,对于C++标准库来说,这是一个杰出的合作伙伴。


Lambda

头文件: "boost/lambda/lambda.hpp"
它包括了本库的核心部分。

"boost/lambda/bind.hpp"
它定义了 bind 函数。

"boost/lambda/if.hpp"
它定义了相当于 if 的 lambda ,以及条件操作符。

"boost/lambda/loops.hpp"
它定义了循环结构(例如,while_loop 和 for_loop)。

"boost/lambda/switch.hpp"
它定义了相当于 switch 语句的 lambda 。

"boost/lambda/construct.hpp"
它定义了一些工具,为增加构造函数/析构函数以及 new/delete 。

"boost/lambda/casts.hpp"
它为提供了转型操作符。

"boost/lambda/exceptions.hpp"
它定义了在 lambda 表达式中进行异常处理的工具。

"boost/lambda/algorithm.hpp" and "boost/lambda/numeric.hpp"
它定义了用于嵌套函数调用的C++标准库算法的 lambda 版本(实际上就是函数对象)。


Boost.Lambda 用法

与其它许多 Boost 库一样,这个库完全定义在头文件中,这意味着你不必构建任何东西就可以开始使用。但是,知道一点关于 lambda 表达式的东西肯定是有帮助的。接下来的章节会带你浏览一下这个库,还包括如何在 lambda 表达式中进行异常处理!这个库非常广泛,前面还有很多强大的东西。一个 lambda 表达式通常也称为匿名函数(unnamed function)。它在需要的时 候进行声明和定义,即就地进行。这非常有用,因为我们常常需要在一个算法中定义另一个算法,这是语言本身所不能支持的。作为替代,我们通过从更大的范围引 进函数和函数对象来具体定义行为,或者使用嵌套的循环结构,把算法表达式写入循环中。我们将看到,这正是 lambda 表达式可以发挥的地方。本节内有许多例子,通常例子的一部分是示范如何用"传统"的编码方法来解决问题。这样做的目的是,看看 lambda 表达式在何时以及如何帮助程序写出更具逻辑且更少的代码。使用 lambda 表达式的确存在一定的学习曲线,而且它的语法初看起来有点可怕。就象每种新的范式或工具,它们都需要去学习,但是请相信我,得到的好处肯定超过付出的代价。

一个简单的开始

第一个使用 Boost.Lambda 的程序将会提升你对 lambda 表达式的喜爱。首先,请注意 lambda 类型是声明在 boost::lambda 名字空间中,你需要用一个 using 指示符或 using 声明来把这些 lambda 声明带入你的作用域。包含头文件 "boost/lambda/lambda.hpp" 就可以使用这个库的主要功能了,对于我们第一个程序这样已经足够了。

#include <iostream>
#include "boost/lambda/lambda.hpp"
#include "boost/function.hpp"
int main() {
  using namespace boost::lambda;
  (std::cout << _1 << " " << _3 << " " << _2 << "!\n")
    ("Hello","friend","my");
  boost::function<void(int,int,int)> f=
    std::cout << _1 << "*" << _2 << "+" << _3
      << "=" <<_1*_2+_3 << "\n";
  f(1,2,3);
  f(3,2,1);
}


第一个表达式看起来很奇特,你可以在脑子里按着括号来划分这个表达式;第一部分就是一个 lambda 表达式,它的意思基本上是说,"打印这些参数到 std::cout, 但不是立即就做,因为我还不知道这三个参数"。表达式的第二部分才是真正调用这个函数,它说,"嘿!这里有你要的三个参数"。我们再来看看这个表达式的第一部分。

  1. std::cout << _1 << " " << _3 << " " << _2 << "!\n"
你会注意到表达式中有三个占位符,命名为 _1, _2, 和 _3 。 这些占位符为 lambda 表达式指出了延后的参数。注意,跟许多函数式编程语言的语法不一样,创建 lambda 表达式时没有关键字或名字;占位符的出现表明了这是一个 lambda 表达式。所以,这是一个接受三个参数的 lambda 表达式,参数的类型可以是任何支持 operator<< 流操作的类型。参数按 1-3-2 的顺序打印到 cout 。在这个例子中,我们把这个表达式用括号括起来,然后调用得到的这个函数对象,传递三个参数给它:"Hello", "friend", 和 "my". 输出的结果如下:

Hello my friend!


通常,我们要把函数对象传入算法,这是我们要进一步研究的,但是我们来试验一些更有用的东西,把 lambda 表达式存入另一个延后调用的函数,名为 boost::function. 这个有用的发明将在下一章 "Library 11: Function 11" 中讨论,现在你只有知道可以传递一个函数或函数对象给 boost::function 的实例并保存它以备后用就可以了。在本例中,我们定义了这样的一个函数 f,象这样:
  1. boost::function<void(int,int,int)> f;
这个声明表示 f 可以存放用三个参数调用的函数和函数对象,参数的类型全部为 int. 然后,我们用一个 lambda 表达式把一个函数对象赋给它,这个表达式表示了算法 X=S*T+U, 并且把这个算式及其结果打印到 cout.

boost::function<void(int,int,int)> f=
  std::cout <<
    _1 << "*" << _2 << "+" << _3 << "=" <<_1*_2+_3 << "\n";


如你所见,在一个表达式中,占位符可以多次使用。我们的函数 f 现在可以象一个普通函数那样调用了,如下:

  1. f(1,2,3);
  2. 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 参数。为方便起见,不论对象是通过引用传递或是通过指针传递,语法都是一样的。因此,在绑定到一个成员函数时,第二个参数(即函数指针后的第一个)就是将要调用该函数的真实对象。绑定到数据成员也是可以的,下面的例子也将有所示范:

#include <iostream>
#include <string>
#include <map>
#include <algorithm>
#include "boost/lambda/lambda.hpp"
#include "boost/lambda/bind.hpp"
int main() {
  using namespace boost::lambda;
  typedef std::map<int,std::string> type;
  type keys_and_values;
  keys_and_values[3]="Less than pi";
  keys_and_values[42]="You tell me";
  keys_and_values[0]="Nothing, if you ask me";
  std::cout << "What's wrong with the following expression?\n";
  std::for_each(
    keys_and_values.begin(),
    keys_and_values.end(),
    std::cout << "key=" <<
      bind(&type::value_type::first,_1) << ", value="
      << bind(&type::value_type::second,_1) << '\n');
  std::cout << "\n...and why does this work as expected?\n";
  std::for_each(
    keys_and_values.begin(),
    keys_and_values.end(),
    std::cout << constant("key=") <<
      bind(&type::value_type::first,_1) << ", value="
      << bind(&type::value_type::second,_1) << '\n');
  std::cout << '\n';
  // Print the size and max_size of the container
  (std::cout << "keys_and_values.size()=" <<
    bind(&type::size,_1) << "\nkeys_and_values.max_size()="
    << bind(&type::max_size,_1))(keys_and_values);
}


这个例子开始时先创建一个 std::map ,键类型为 int 且值类型为 std::string 。记住,std::map 的 value_type 是一个由键类型和值类型组成的 std::pair 。因此,对于我们的 map, value_type 就是 std::pair<int,std::string>, 所以在 for_each 算法中,我们传入的函数对象要接受一个这样的类型。给出这个 pair, 就可以取出其中的两个成员(键和值),这正是我们第一个 bind 表达式所做的。

  1. bind(&type::value_type::first,_1)
这个表达式生成一个函数对象,它被调用时将取出它的参数,即我们前面讨论的 pair 中的嵌套类型 value_type 的数据成员 first。在我们的例子中,first 是 map 的键类型,它是一个 const int. 对于成员函数,语法是完全相同的。但你要留意,我们的 lambda 表达式多做了一点;表达式的第一部分是
  1. std::cout << "key=" << ...
它可以编译,也可以工作,但它可能不能达到目的。这个表达式不是一个 lambda 表达式;它只是一个表达式而已,再没有别的了。执行时,它打印 key=, 当这个表达式被求值时它仅执行一次,而不是对于每个被 std::for_each 所访问的元素执行一次。在这个例子中,原意是把 key= 作为我们的每一个 keys_and_values 键/值对的前缀。在早一点的那些例子中,我们也是这样写的,但那里没有出现这些问题。原因在于,那里我们用了一个占位符来作为 operator<< 的第一个参数,这样就使得它成为一个有效的 lambda 表达式。而这里,我们必须告诉 Boost.Lambda 要创建一个包含 "key=" 的函数对象。这就要使用函数 constant, 它创建一个无参函数对象,即不带参数的函数对象;它仅仅保存其参数,然后在被调用时返回它。
  1. 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.ma
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值