Boost.Lambda 用法详解(二)

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_functionbinary_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 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 特化。这个类型可以是 intstd::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 版本。要使用选择语句,ifswitch, 就分别包含头文件 "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));
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值