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 类是一个好主意;它很有帮助。
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 表达式的复杂度,它需要更长一点的时间来掌握它的用法。当你掌握了它以后,它就变得很自然了(编写它们也一样!)。采用哪一种格式完全取决于你的爱好;它们做得是同一件事。
在上例中,我们有一个 string 的 vector,如果 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, 它调用条件操作符。让我们来仔细看看所有控制结构,从 if 和 switch 开始。记住,要使用 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_(_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));