Boost.Lambda 用法详解(二)

来源:互联网 发布:2016网络信息安全事件 编辑:程序博客网 时间:2024/05/21 07:13

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_typeresult_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 inttemplate<> class sig_helper<int> {public:  typedef std::string type;};// The version for the overload on std::stringtemplate<> class sig_helper<std::string> {public:  typedef double type;};// The function objectclass 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 的第二个元素)被取出,并去掉所有的 constvolatile 限定符,结果类型被用于实例化正确版本的sig_helper 类,后者具有正确的 typedef type. 这是为我们的类定义返回类型的一种相当复杂(但是必须!)的方法,但是多数情况下,通常都只有一个版本的调用操作符;所以正确地增加嵌套sig 类是一件普通的工作。

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

Lambda 表达式中的控制结构

我们已经看到强大的lambda 表达式可以很容易地创建,但是许多编程上的问题需要我们可以表示条件,在C++中我们使用 if-then-else,for, while,等等。在 Boost.Lambda 中有所有的C++控制结构的 lambda 版本。要使用选择语句,if switch,就分别包含头文件 "boost/lambda/if.hpp""boost/lambda/switch.hpp"。要使用循环语句,while,do, for,就包含头文件 "boost/lambda/loops.hpp".关键字不能被重载,所以语法与你前面使用过的有所不同,但是也有很明显的关联。作为第一个例子,我们来看看如何在 lambda 表达式中创建一个简单的 if-then-else 结构。格式是 if_then_else(条件, then-语句, else-语句)。还有另外一种语法形式,即if_(条件)[then-语句].else_[else-语句]

#include <iostream>#include <algorithm>#include <vector>#include <string>#include "boost/lambda/lambda.hpp"#include "boost/lambda/bind.hpp"#include "boost/lambda/if.hpp"int main() {  using namespace boost::lambda;  std::vector<std::string> vec;  vec.push_back("Lambda");  vec.push_back("expressions");  vec.push_back("really");  vec.push_back("rock");  std::for_each(vec.begin(),vec.end(),if_then_else(    bind(&std::string::size,_1)<=6u,    std::cout << _1 << '\n',    std::cout << constant("Skip.\n")));  std::for_each(vec.begin(),vec.end(),    if_(bind(&std::string::size,_1)<=6u) [      std::cout << _1 << '\n'    ]    .else_[      std::cout << constant("Skip.\n")    ] );}

如果你是从本章开头一直读到这的,你可能会觉得上述代码非常好读;但如果你是跳到这来的,就可能觉得惊讶了。控制结构的确增加了阅读lambda 表达式的复杂度,它需要更长一点的时间来掌握它的用法。当你掌握了它以后,它就变得很自然了(编写它们也一样!)。采用哪一种格式完全取决于你的爱好;它们做得是同一件事。

在上例中,我们有一个 stringvector,如果 string 元素的大小小于等于6,它们就被输出到std::cout; 否则,输出字符串 "Skip"。在这个 if_then_else表达式中有一些东西值得留意。

if_then_else(  bind(&std::string::size,_1)<=6u,  std::cout << _1 << '\n',  std::cout << constant("Skip.\n")));

首先,条件是一个谓词,它必须是一个 lambda 表达式!其次,then-语句必须也是一个lambda 表达式!第三,else-语句必须也是一个 lambda 表达式!头两个都很容易写出来,但最后一个很容易忘掉用 constant来把字符串("Skip\n")变成一个 lambda 表达式。细心的读者会注意到例子中使用了 6u, 而不是使用 6,这是为了确保执行的是两个无符号类型的比较。这样做的原因是,我们使用的是非常深的嵌套模板,这意味着如果这样一个 lambda 表达式引发了一个编译器警告,输出信息将会非常、非常长。你可以试一下去掉这个 u,看看你的编译器会怎样!你将看到一个关于带符号类型与无符号类型比较的警告,因为std::string::size 返回一个无符号类型。

控制结构的返回类型是void, 除了 if_then_else_return,它调用条件操作符。让我们来仔细看看所有控制结构,从 ifswitch 开始。记住,要使用if-结构,必须包含 "boost/lambda/if.hpp"。对于switch, 必须包含 "boost/lambda/switch.hpp"。以下例子都假定名字空间boost::lambda 中的声明已经通过using声明或using指令,被带入当前名字空间。

(if_then(_1<5,  std::cout << constant("Less than 5")))(make_const(3));

if_then函数以一个条件开始,后跟一个 then-部分;在上面的代码中,如果传给该 lambda 函数的参数小于5 (_1<5), "Less than 5"将被输出到 std::cout. 你会看到如果我们用数值3调用这个 lambda 表达式,我们不能直接传递3,象这样。

(if_then(_1<5,std::cout << constant("Less than 5")))(3);

这会引起一个编译错误,因为3是一个 int,而一个类型 int (或者任何内建类型)的左值不能被const 限定。因此,我们在这里必须使用工具 make_const,它只是返回一个对它的参数的 const 引用。另一个方法是把整个 lambda表达式用于调用 const_parameters,象这样:

(const_parameters(  if_then(_1<5,std::cout << constant("Less than 5"))))(3);

const_parameters对于避免对多个参数分别进行 make_const 非常有用。注意,使用该函数时,lambda表达式的所有参数都被视为 const 引用。

现在来看另一种语法的 if_then

(if_(_1<5)  [std::cout << constant("Less than 5")])(make_const(3));

这种写法更类似于C++关键字,但它与 if_then所做的完全一样。函数 if_ (注意最后的下划线)后跟括起来的条件,再后跟then-语句。重复一次,选择哪种语法完全取决于你的口味。

现在,让我们来看看 if-then-else结构;它们与 if_then 很相似。

(if_then_else(  _1==0,  std::cout << constant("Nothing"),  std::cout << _1))(make_const(0));(if_(_1==0)  [std::cout << constant("Nothing")].  else_[std::cout << _1])(make_const(0));