Beyond the C++ Standard Library: An Introduction to Boost -- Library 10.2 Usage(上)

用法

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

A Little Teaser

第一个使用 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, 但不是立即就做,因为我还不知道这三个参数"。表达式的第二部分才是真正调用这个函数,它说,"嘿!这里有你要的三个参数"。我们再来看看这个表达式的第一部分。

std::cout << _1 << " " << _3 << " " << _2 << "!/n"

你会注意到表达式中有三个占位符,命名为 _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<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 现在可以象一个普通函数那样调用了,如下:

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

#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 。因此,对于我们的 mapvalue_type 就是 std::pair<int,std::string>, 所以在 for_each 算法中,我们传入的函数对象要接受一个这样的类型。给出这个 pair, 就可以取出其中的两个成员(键和值),这正是我们第一个 bind 表达式所做的。

bind(&type::value_type::first,_1)

这个表达式生成一个函数对象,它被调用时将取出它的参数,即我们前面讨论的 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] 你也得小心行事。我们只是说它在技术上可行。

class double_it {
public:
int operator()(int i) const {
return i*2;
}
};
int main() {
using namespace boost::lambda;
double_it d;
int i=12;
// If you uncomment the following expression,
// the compiler will complain;
// it's just not possible to deduce the return type
// of the function call operator of double_it.
// (std::cout << _1 << "*2=" << (bind(d,_1)))(i);
(std::cout << _1 << "*2=" << (bind<int>(d,_1)))(i);
(std::cout << _1 << "*2=" << (ret<int>(bind(d,_1))))(i);
}

有两种版本的方法来关闭返回类型推断系统,短格式的版本只需把返回类型作为模板参数传给 bind, 另一个版本则使用 ret, 它要括住不能进行自动推断的 lambda/bind 表达式。在嵌套的 lambda 表达式中,这很容易会就得乏味,不过还有一种更好的方法可以让推断成功。我们将在本章稍后进行介绍。

请注意,一个绑定表达式可以由另一个绑定表达式组成,这使得绑定器成为了进行函数组合的强大工具。嵌套的绑定有许多强大的功能,但是要小心使用,因为这些强大的功能同时也带来了读写以及理解代码上的额外的复杂性。

我不喜欢 _1, _2, and _3,我可以用别的名字吗?

有的人对预定义的占位符名称不满意,因此本库提供了简便的方法来把它们[3]改为任意用户想用的名字。这是通过声明一些类型为 boost::lambda::placeholderX_type 的变量来实现的,其中 X 1, 2, 3. 例如,假设某人喜欢用 Arg1, Arg2, Arg3 来作占位符的名字:

[3] 技术上,是增加新的名字。

#include <iostream>
#include <vector>
#include <string>
#include "boost/lambda/lambda.hpp"
boost::lambda::placeholder1_type Arg1;
boost::lambda::placeholder2_type Arg2;
boost::lambda::placeholder3_type Arg3;
template <typename T,typename Operation>
void for_all(T& t,Operation Op) {
std::for_each(t.begin(),t.end(),Op);
}
int main() {
std::vector<std::string> vec;
vec.push_back("What are");
vec.push_back("the names");
vec.push_back("of the");
vec.push_back("placeholders?");
for_all(vec,std::cout << Arg1 << " ");
std::cout << "/nArg1, Arg2, and Arg3!";
}

你定义的占位符变量可以象 _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<T>::type boost::lambda::var_type<T>::type 的变量,其中的 T 为被包装的常量或变量的类型。看一下以下这个 lambda 表达式的用法:

for_all(vec,
std::cout << constant(' ') << _ << constant('/n'));

总是使用 constant 会很让人讨厌。下面是一个例子,它命名了两个常量,newline space,并把它们用于 lambda 表达式。

#include <iostream>
#include <vector>
#include <algorithm>
#include "boost/lambda/lambda.hpp"
int main() {
using boost::lambda::constant;
using boost::lambda::constant_type;
constant_type<char>::type newline(constant('/n'));
constant_type<char>::type space(constant(' '));
boost::lambda::placeholder1_type _;
std::vector<int> vec;
vec.push_back(0);
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
for_all(vec,std::cout << space << _ << newline);
for_all(vec,
std::cout << constant(' ') << _ << constant('/n'));
}

这是一个避免重复键入的好方法,也可以使 lambda 表达式更清楚些。下面是一个类似的例子,首先定义一个类型 memorizer, 用于跟踪曾经赋给它的所有值。然后,用 var_type 创建一个命名变量,用于后面的 lambda 表达式。你将会看到命名常量要比命名变量更常用到,但也有些情形会需要使用命名变量[4]

[4] 特别是使用 lambda 循环结构时。

#include <iostream>
#include <vector>
#include <algorithm>
#include "boost/lambda/lambda.hpp"
template <typename T> class memorizer {
std::vector<T> vec_;
public:
memorizer& operator=(const T& t) {
vec_.push_back(t);
return *this;
}
void clear() {
vec_.clear();
}
void report() const {
using boost::lambda::_1;
std::for_each(
vec_.begin(),
vec_.end(),
std::cout << _1 << ",");
}
};
int main() {
using boost::lambda::var_type;
using boost::lambda::var;
using boost::lambda::_1;
std::vector<int> vec;
vec.push_back(0);
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
memorizer<int> m;
var_type<memorizer<int> >::type mem(var(m));
std::for_each(vec.begin(),vec.end(),mem=_1);
m.report();
m.clear();
std::for_each(vec.begin(),vec.end(),var(m)=_1);
m.report();
}

这就是它的全部了,但在你认为自己已经明白了所有东西之前,先回答这个问题:在以下声明下 T 应该是什么类型?

constant_type<T>::type hello(constant("Hello"));

它是一个 char*? 一个 const char*? 都不是,它的正确类型是一个含有六个字符(还有一个结束用的空字符)的数组的常量引用,所以我们应该这样写:

constant_type<const char (&)[6]>::type
hello(constant("Hello"));

这很不好看,而且对于需要修改这个字符串的人来说也很痛苦,所以我更愿意使用 std::string 来写。

constant_type<std::string>::type
hello_string(constant(std::string("Hello")));

这次,你需要比上一次多敲几个字,但你不需要再计算字符的个数,如果你要改变这个字符串,也没有问题。

ptr_fun mem_fun 到哪去了?

也许你还在怀念它们,由于 Boost.Lambda 创建了与标准一致的函数对象,所以没有必要再记住这些标准库中的适配器类型了。一个绑定了函数或成员函数的 lambda 表达式可以很好地工作,而且不论绑定的是什么类型,其语法都是一致的。这可以让代码更注重其任务而不是某些奇特的语法。以下例子说明了这些好处:

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
#include "boost/lambda/lambda.hpp"
#include "boost/lambda/bind.hpp"
void plain_function(int i) {
std::cout << "void plain_function(" << i << ")/n";
}
class some_class {
public:
void member_function(int i) const {
std::cout <<
"void some_class::member_function(" << i << ") const/n";
}
};
int main() {
std::vector<int> vec(3);
vec[0]=12;
vec[1]=10;
vec[2]=7;
some_class sc;
some_class* psc=&sc;
// Bind to a free function using ptr_fun
std::for_each(
vec.begin(),
vec.end(),
std::ptr_fun(plain_function));
// Bind to a member function using mem_fun_ref
std::for_each(vec.begin(),vec.end(),
std::bind1st(
std::mem_fun_ref(&some_class::member_function),sc));
// Bind to a member function using mem_fun
std::for_each(vec.begin(),vec.end(),
std::bind1st(
std::mem_fun(&some_class::member_function),psc));
using namespace boost::lambda;
std::for_each(
vec.begin(),
vec.end(),
bind(&plain_function,_1));
std::for_each(vec.begin(),vec.end(),
bind(&some_class::member_function,sc,_1));
std::for_each(vec.begin(),vec.end(),
bind(&some_class::member_function,psc,_1));
}

这里真的不需要用 lambda 表达式吗?相对于使用三个不同的结构来完成同一件事情,我们可以只需向 bind 指出要干什么,然后它就会去做。在这个例子中,需要用 std::bind1st 来把 some_class 的实例绑定到调用中;而对于 Boost.Lambda,这是它工作的一部分。因此,下次你再想是否要用 ptr_fun, mem_fun, mem_fun_ref 时,停下来,使用 Boost.Lambda 来代替它!

无须<functional>的算术操作

我们常常要按顺序对一些元素执行算术操作,而标准库提供了多个函数对象来执行算术操作,如 plus, minus, divides, modulus, 等等。但是,这些函数对象需要我们多打很多字,而且常常需要绑定一个参数,这时应该使用绑定器。如果要嵌套这些算术操作,表达式很快就会变得难以使用,而 这正是 lambda 表达式可以发挥巨大作用的地方。因为我们正在处理的是操作符,既是算术上的也是C++术语上的,所以我们有能力使用 lambda 表达式直接编写我们的算法代码。作为一个小的动机,考虑一个简单的问题,对一个数值增加4。然后再考虑另一个问题,完成与标准库算法(transform)同样的工作。虽然第一个问题非常自然,而第二个则完全不一样(它需要你手工写循环)。但使用 lambda 表达式,只需关注算法本身。在下例中,我们先使用 std::bind1st std::plus 对容器中的每个元素加4,然后我们使用 lambda 来减4

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
#include "boost/lambda/lambda.hpp"
#include "boost/lambda/bind.hpp"
int main() {
using namespace boost::lambda;
std::vector<int> vec(3);
vec[0]=12;
vec[1]=10;
vec[2]=7;
// Transform using std::bind1st and std::plus
std::transform(vec.begin(),vec.end(),vec.begin(),
std::bind1st(std::plus<int>(),4));
// Transform using a lambda expression
std::transform(vec.begin(),vec.end(),vec.begin(),_1-=4);
}

差别是令人惊讶的!在使用"传统"方法进行加4时,对于未经训练的眼睛来说,很难看出究竟在干什么。从代码中我们看到,我们将一个缺省构造的 std::plus 实例的第一个参数绑定到4。而 lambda 表达式则写成从元素减4。如果你认为使用 bind1st plus 的版本还不坏,你可以试试更长的表达式。

Boost.Lambda 支持C++中的所有算术操作符,因此几乎不再需要仅为了算术函数对象而包含 <functional> 。以下例子示范了这些算术操作符中某些的用法。vector vec 中的每个元素被加法和乘法操作符修改。

#include <iostream>
#include <vector>
#include <algorithm>
#include "boost/lambda/lambda.hpp"
int main() {
using namespace boost::lambda;
std::vector<int> vec(3);
vec[0]=1;
vec[1]=2;
vec[2]=3;
std::for_each(vec.begin(),vec.end(),_1+=10);
std::for_each(vec.begin(),vec.end(),_1-=10);
std::for_each(vec.begin(),vec.end(),_1*=3);
std::for_each(vec.begin(),vec.end(),_1/=2);
std::for_each(vec.begin(),vec.end(),_1%=3);
}

简洁、可读、可维护,这就是使用 Boost.Lambda 所得到的代码的风格。跳过 std::plus, std::minus, std::multiplies, std::divides, std::modulus; 使用 Boost.Lambda,你的代码总会更好。

编写可读的谓词

标准库中的许多算法都有一个版本是接受一个一元或二元的谓词的。这些谓词是普通函数或函数对象,当 然,lambda 表达式也可以。对于会经常用到的谓词,当然应该定义函数对象,但通常,它们只使用一两次并且再不会碰到。在这种情况下,lambda 表达式是更好的选择,这既是因为代码可以更容易理解(所有功能都在同一个地方),也是因为代码不会被一些极少使用的函数对象搞混。作为一个具体的例子,我 们在容器中查找具有某个特定值的元素。如果该元素类型已经定义了 operator== ,则可以直接使用算法 find ,但如果要使用其它标准来查找元素呢?以下给出类型 search_for_me ,你如何使用 find 来查找第一个元素,其满足成员函数 a 返回 "apple"的条件?

#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
class search_for_me {
std::string a_;
std::string b_;
public:
search_for_me() {}
search_for_me(const std::string& a,const std::string& b)
: a_(a),b_(b) {}
std::string a() const {
return a_;
}
std::string b() const {
return b_;
}
};
int main() {
std::vector<search_for_me> vec;
vec.push_back(search_for_me("apple","banana"));
vec.push_back(search_for_me("orange","mango"));
std::vector<search_for_me>::iterator it=
std::find_if(vec.begin(),vec.end(),???);
if (it!=vec.end())
std::cout << it->a() << '/n';
}

首先,我们需要用 find_if,[5] 但是标记了 ??? 的地方应该怎样写呢?一种办法是:用一个函数对象来实现该谓词的逻辑。

[5] find 使用 operator==; find_if 则要求一个谓词函数(或函数对象)

class a_finder {
std::string val_;
public:
a_finder() {}
a_finder(const std::string& val) : val_(val) {}
bool operator()(const search_for_me& s) const {
return s.a()==val_;
}
};

这个函数对象可以这样使用:

std::vector<search_for_me>::iterator it=
std::find_if(vec.begin(),vec.end(),a_finder("apple"));

这可以,但两分钟(或几天)后,我们想要另一个函数对象,这次要测试成员函数 b. 等等…这类事情很快就会变得乏味。正如你确信的那样,这是 lambda 表达式的另一个极好的例子;我们需要某种灵活性,可以在需要的地方和需要的时间直接创建谓词。我们可以这样来写前述的 find_if

std::vector<search_for_me>::iterator it=
std::find_if(vec.begin(),vec.end(),
bind(&search_for_me::a,_1)=="apple");

我们 bind 到成员函数 a, 并且测试它是否等于 "apple",这就是我们的一元谓词,它就定义在使用的地方。但是等一下,正如它们说的,还有更多的东西。在处理数值类型时,我们可以在所有算术操作符、比较和逻辑操作符中选择。这意味着哪怕是复杂的谓词也可以直接了当地定义。仔细阅读以下代码,看看谓词是如何表示的。

#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include "boost/lambda/lambda.hpp"
int main() {
using namespace boost::lambda;
std::vector<int> vec1;
vec1.push_back(2);
vec1.push_back(3);
vec1.push_back(5);
vec1.push_back(7);
vec1.push_back(11);
std::vector<int> vec2;
vec2.push_back(7);
vec2.push_back(4);
vec2.push_back(2);
vec2.push_back(3);
vec2.push_back(1);
std::cout << *std::find_if(vec1.begin(),vec1.end(),
(_1>=3 && _1<5) || _1<1) << '/n';
std::cout << *std::find_if(vec2.begin(),vec2.end(),
_1>=4 && _1<10) << '/n';
std::cout << *std::find_if(vec1.begin(),vec1.end(),
_1==4 || _1==5) << '/n';
std::cout << *std::find_if(vec2.begin(),vec2.end(),
_1!=7 && _1<10) << '/n';
std::cout << *std::find_if(vec1.begin(),vec1.end(),
!(_1%3)) << '/n';
std::cout << *std::find_if(vec2.begin(),vec2.end(),
_1/2<3) << '/n';
}

如你所见,创建这些谓词就象写出相应的逻辑一样容易。这正是我喜欢使用 lambda 表达式的地方,因为它可以被任何人所理解。有时候我们也需要选择 lambda 表达式以外的机制,因为那些必须理解这些代码的人的能力;但是在这里,除了增加的价值以外没有其它了。

让你的函数对象可以与 Boost.Lambda 一起使用

不是所有的表达式都适合使用 lambda 表达式,复杂的表达式更适合使用普通的函数对象,而且会多次重用的表达式也应该成为你代码中的一等公民。它们应该被收集为一个可重用函数对象的库。但是, 你也可能想把这些函数对象用在 lambda 表达式中,你希望它们可以与 Lambda 一起使用;不是所有函数对象都能做到。问题是函数对象的返回类型不能象普通函数那样被推断出来;这是语言的固有限制。但是,有一个定义好的方法来把这个重 要的信息提供给 Lambda 库,以使得 bind 表达式更加干净。作为这个问题的一个例子,我们看以下函数对象:

template <typename T> class add_prev {
T prev_;
public:
T operator()(T t) {
prev_+=t;
return prev_;
}
};

对于这样一个函数对象,lambda 表达式不能推断出返回类型,因此以下例子不能编译。

#include <iostream>
#include <algorithm>
#include <vector>
#include "boost/lambda/lambda.hpp"
#include "boost/lambda/bind.hpp"
int main() {
using namespace boost::lambda;
std::vector<int> vec;
vec.push_back(5);
vec.push_back(8);
vec.push_back(2);
vec.push_back(1);
add_prev<int> ap;
std::transform(
vec.begin(),
vec.end(),
vec.begin(),
bind(var(ap),_1));
}

问题在于对 transform 的调用。

std::transform(vec.begin(),vec.end(),vec.begin(),bind(var(ap),_1));

当绑定器被实例化时,返回类型推断的机制被使用…而且失败了。因此,这段程序不能通过编译,你必须显式地告诉 bind 返回类型是什么,象这样:

std::transform(vec.begin(),vec.end(),vec.begin(),
bind<int>(var(ap),_1));

这是为 lambda 表达式显式设置返回类型的正常格式的缩写,它等价于这段代码。

std::transform(vec.begin(),vec.end(),vec.begin(),
ret<int>(bind<int>(var(ap),_1)));

这并不是什么新问题;对于在标准库算法中使用函数对象都有同样的问题。在标准库中,解决的方法是增加 typedefs 来表明函数对象的返回类型及参数类型。标准库还提供了助手类来完成这件事,即类模板 unary_function binary_function,要让我们的例子类 add_prev 成为合适的函数对象,可以通过定义所需的 typedefs (对于一元函数对象,是argument_type result_type,对于二元函数对象,是first_argument_type, second_argument_type, result_type),也可以通过派生自 unary_function/binary_function 来实现。

template <typename T> class add_prev : public std::unary_function<T,T>

这对于 lambda 表达式是否也足够好了呢?我们可以简单地复用这种方法以及我们已有的函数对象吗?唉,答案是否定的。这种 typedef 方法有一个问题:对于泛化的调用操作符,当返回类型或参数类型依赖于模板参数时会怎么样?或者,当存在多个重载的调用操作符时会怎么样?由于语言支持模板的 typedefs, 这些问题可以解决,但是现在不是这样的。这就是为什么 Boost.Lambda 需要一个不同的方法,即一个名为 sig 的嵌套泛型类。为了让返回类型推断可以和 add_prev 一起使用,我们象下面那样定义一个嵌套类型 sig

template <typename T> class add_prev :
public std::unary_function<T,T> {
T prev_;
public:
template <typename Args> class sig {
public:
typedef T type;
};
// Rest of definition

模板参数 Args 实际上是一个 tuple,包含了函数对象(第一个元素)和调用操作符的参数类型。在这个例子中,我们不需要这些信息,返回类型和参数类型都是 T. 使用这个改进版本的 add_prev, 再不需要在 lambda 表达式中使用返回类型推断的缩写,因此我们最早那个版本的代码现在可以编译了。

std::transform(vec.begin(),vec.end(),vec.begin(),bind(var(ap),_1));

我们再来看看 tuple 作为 sig 的模板参数是如何工作的,来看另一个有两个调用操作符的函数对象,其中一个版本接受一个 int 参数,另一个版本接受一个 const std::string 引用。我们必须要解决的问题是,"如果传递给 sig 模板的 tuple 的第二个元素类型为 int, 则设置返回类型为 std::string; 如果传递给 sig 模板的 tuple 的第二个元素类型为 std::string, 则设置返回类型为 double"。为此,我们增加一个类模板,我们可以对它进行特化并在 add_prev::sig 中使用它。

template <typename T> class sig_helper {};
// The version for the overload on int
template<> class sig_helper<int> {
public:
typedef std::string type;
};
// The version for the overload on std::string
template<> class sig_helper<std::string> {
public:
typedef double type;
};
// The function object
class some_function_object {
template <typename Args> class sig {
typedef typename boost::tuples::element<1,Args>::type
cv_first_argument_type;
typedef typename
boost::remove_cv<cv_first_argument_type>::type
first_argument_type;
public:
// The first argument helps us decide the correct version
typedef typename
sig_helper<first_argument_type>::type type;
};
std::string operator()(int i) const {
std::cout << i << '/n';
return "Hello!";
}
double operator()(const std::string& s) const {
std::cout << s << '/n';
return 3.14159265353;
}
};

这里有两个重要的部分要讨论:首先是助手类 sig_helper, 它由类型 T 特化。这个类型可以是 int std::string, 依赖于要使用哪一个重载版本的调用操作符。通过对这个模板进行全特化,来定义正确的 typedef type。第二个要注意的部分是 sig 类,它的第一个参数(tuple 的第二个元素)被取出,并去掉所有的 const volatile 限定符,结果类型被用于实例化正确版本的 sig_helper 类,后者具有正确的 typedef type. 这是为我们的类定义返回类型的一种相当复杂(但是必须!)的方法,但是多数情况下,通常都只有一个版本的调用操作符;所以正确地增加嵌套 sig 类是一件普通的工作。

我们的函数对象可以在 lambda 表达式中正确使用是很重要的,在需要时定义嵌套 sig 类是一个好主意;它很有帮助。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值