Lambda 表达式中的类型转换
在 lambda 表达式中有四种特殊的"转型操作符"[7] 来进行类型的转换:ll_dynamic_cast, ll_static_cast, ll_reinterpret_cast, 和 ll_const_cast. 这些名字与对应的C++关键字不一样,因为它们不能被重载。要使用这些类型转换,就要包含头文件 "boost/lambda/casts.hpp". 这些函数与相对应的C++转型操作符用法类似;它们带一个显式的模板参数,即要转成的目标类型,以及一个隐式的模板参数,即源类型。在我们的第一个例子中,我们将使用两个类,名为 base 和 derived. 我们将创建两个指向 base 的指针,一个指向 base 实例,另一个指向 derived 实例。然后我们尝试使用 ll_dynamic_cast 来从这两个指针分别获得一个 derived* 。
#include <iostream> #include "boost/lambda/lambda.hpp" #include "boost/lambda/casts.hpp" #include "boost/lambda/if.hpp" #include "boost/lambda/bind.hpp" class base { public: virtual ~base() {} void do_stuff() const { std::cout << "void base::do_stuff() const\n"; } }; class derived : public base { public: void do_more_stuff() const { std::cout << "void derived::do_more_stuff() const\n"; } }; int main() { using namespace boost::lambda; base* p1=new base; base* p2=new derived; derived* pd=0; (if_(var(pd)=ll_dynamic_cast<derived*>(_1)) [bind(&derived::do_more_stuff,var(pd))]. else_[bind(&base::do_stuff,*_1)])(p1); (if_(var(pd)=ll_dynamic_cast<derived*>(_1)) [bind(&derived::do_more_stuff,var(pd))]. else_[bind(&base::do_stuff,*_1)])(p2); }
在 main 中,我们做的第一件事情是创建 p1 和 p2; p1 指向一个 base, 而 p2 则指向一个 derived. 在第一个 lambda 表达式中,被赋值的 pd 变成了条件;它被隐式转换为 bool, 如果它为 true, then-部分被求值。这里,我们 bind 到成员函数 do_more_stuff. 如果 ll_dynamic_cast 失败了,延期变量 pd 将为 0, 则 else-部分被执行。因此,在我们例子中,lambda 表达式的第一次执行将调用 base 上的 do_stuff ,而第二次执行则调用 derived 上的 do_more_stuff ,运行这个程序的输出如下。
void base::do_stuff() const void derived::do_more_stuff() const
注意,在这个例子中,参数 _1 被解引用,但这并不是必须的;如果需要它会隐式地完成。如果一个 bind 表达式的某个参数必须总是一个指针类型,你可以自己强制对它解引用。否则,把这件杂事留给 Boost.Lambda.
ll_static_cast 对于避免警告非常有用。不要用它来抑制重要的信息,但可以用来减少噪音。在前面的一个例子中,我们创建了一个 bind 表达式来求一个 std::string 的长度(使用 std::string::size)并将该长度与另一个整数进行比较。std::string::size 的返回类型是一个无符号类型,把它与一个有符号整数进行比较(这很常见),编译器会产生一个警告,说有符号与无符号的比较是危险的动作。但是,因为这发生在一个 lambda 表达式中,编译器忠实地跟踪到问题的根源,告诉你某个嵌套模板的某部分应对此严重问题负责。结果是一个非常长的警告信息,由于低信噪比的原因,它可能会掩盖了其它的问题。在泛型代码中,这有时会成为问题,因为所使用的类型不在我们的控制之内。因而,在评估过可能潜在的问题后,你常会发现使用 ll_static_cast 来抑制不想要的警告是有好处的。以下例子包含了示范这种行为的代码。
#include <iostream> #include <string> #include <algorithm> #include "boost/lambda/lambda.hpp" #include "boost/lambda/casts.hpp" #include "boost/lambda/if.hpp" #include "boost/lambda/bind.hpp" template <typename String,typename Integral> void is_it_long(const String& s,const Integral& i) { using namespace boost::lambda; (if_then_else(bind(&String::size,_1)<_2, var(std::cout) << "Quite short...\n", std::cout << constant("Quite long...\n")))(s,i); } int main() { std::string s="Is this string long?"; is_it_long(s,4u); is_it_long(s,4); }
泛型函数 is_it_long (请不要关注它是一个有点过于做作的例子)用一个 Integral 类型的常数变量引用来执行一个 lambda 表达式。现在,这个类型是有符号的还是无符号的并不在我们的控制之内,因此很有机会某个用户会不小心引发了一个非常冗长的警告,正如这个例子所示范的,因为用了一个有符号整数调用 is_it_long 。
is_it_long(s,4);
确保用户不会无意地引发此事(除了要求只能使用无符号类型)的唯一办法是让这个参数变成无符号整数类型,不管它原来是什么。这正是 ll_static_cast 的工作,因此我们把函数 is_it_long 改成这样:
template <typename String,typename Integral> void is_it_long(const String& s,const Integral& i) { using namespace boost::lambda; (if_then_else(bind(&String::size,_1)< ll_static_cast<typename String::size_type>(_2), var(std::cout) << "Quite short...\n", std::cout << constant("Quite long...\n")))(s,i); }
这种情况不会经常发生(至少我没碰到几次),但只要它发生了,这种解决方法就可用。ll_const_cast 和 ll_reinterpret_cast 的用法与我们已经看到的相似,所以就不再举例了。小心地使用它们,如果没有非常必要的原因(我想不到有什么原因),尽量不要使用 ll_reinterpret_cast 。这是对称的;如果你需要用它,很大机会你做了一些不应该做的事情。
构造与析构
当有必要在 lambda 表达式中创建或销毁对象时,就需要一些特殊的处理和语法。首先,你不可能获取构造函数或析构函数的地址,也就不可能对它们使用标准的 bind 表达式。此外,操作符 new 和 delete 有固定的返回类型,因此它们对于任何类型都不能返回 lambda 表达式。如果你需要在 lambda 表达式中创建或销毁对象,先要确保包含头文件 "boost/lambda/construct.hpp", 它包含了模板 constructor, destructor, new_ptr, new_array, delete_ptr, 以及 delete_array. 我们来看看如何使用它们,并主要关注 constructor 和 new_ptr, 它们在构造对象时是最常用的。
我们的第一个例子是一个以智能指针作为元素的容器,我们想在 lambda 表达式中重设智能指针的内容。这通常需要一个对 operator new 的调用;例外的情形是使用了客户化的分配机制,或者是某种工厂方法(factory method)。我们要用 new_ptr 来做,如果你想要或者需要的话,通常也可以在赋值表达式中使用 constructor 。我们两种方法都试一下。我们将定义两个类,base 和 derived, 以及一个 boost::shared_ptr<base> 的 std::map ,它以 std::strings 为索引。在阅读本例中的 lambda 表达式之前,先来一下深呼吸;它们将是你在本章所见到的最复杂的两个 lambda 表达式。虽然复杂,但是要理解它们是干什么的还是很明白的。只是要花你一点时间。
#include <iostream> #include <map> #include <string> #include <algorithm> #include "boost/lambda/lambda.hpp" #include "boost/lambda/construct.hpp" #include "boost/lambda/bind.hpp" #include "boost/lambda/if.hpp" #include "boost/lambda/casts.hpp" #include "boost/shared_ptr.hpp" class base { public: virtual ~base() {} }; class derived : public base { }; int main() { using namespace boost::lambda; typedef boost::shared_ptr<base> ptr_type; typedef std::map<std::string,ptr_type> map_type; map_type m; m["An object"]=ptr_type(new base); m["Another object"]=ptr_type(); m["Yet another object"]=ptr_type(new base); std::for_each(m.begin(),m.end(), if_then_else(!bind(&ptr_type::get, bind(&map_type::value_type::second,_1)), (bind(&map_type::value_type::second,_1)= bind(constructor<ptr_type>(),bind(new_ptr<derived>())), var(std::cout) << "Created a new derived for \"" << bind(&map_type::value_type::first,_1) << "\".\n"), var(std::cout) << "\"" << bind(&map_type::value_type::first,_1) << "\" already has a valid pointer.\n")); m["Beware, this is slightly tricky"]=ptr_type(); std::cout << "\nHere we go again...\n"; std::for_each(m.begin(),m.end(), if_then_else(!bind(&map_type::value_type::second,_1), ((bind(static_cast<void (ptr_type::*)(base*)> (&ptr_type::reset<base>), bind(&map_type::value_type::second,_1), bind(new_ptr<base>()))), var(std::cout) << "Created a new derived for \"" << bind(&map_type::value_type::first,_1) << "\".\n"), var(std::cout) << "\"" << bind(&map_type::value_type::first,_1) << "\" already has a valid pointer.\n")); }
你都看懂了,是吗?以防万一有些混乱,我来解释一下在这个例子中发生了什么。首先,这两个 lambda 表达式做的是同一件事情。它们对 std::map 中的每一个当前为 null 的元素设置有效的指针。以下是程序运行的输出:
"An object" already has a valid pointer.
"Another object" already has a valid pointer.
"Yet another object" already has a valid pointer.
// 译注:前面这三行在原书中有重复,共六行,与程序运行结果不符
Here we go again...
"An object" already has a valid pointer.
"Another object" already has a valid pointer.
Created a new derived for "Beware, this is slightly tricky".
"Yet another object" already has a valid pointer.
输出显示我们设法把有效的对象放入 map 的每一个元素,但是这是如何办到的呢?
两个表达式完成了相同的工作,但各自有不同的方法。从第一个开始,我们来把这个 lambda 表达式切开来看看它是如何工作的。当然,第一部分是条件,它很普通:[8]
!bind(&ptr_type::get,bind(&map_type::value_type::second,_1))
这样更容易看了,对吗?从最里面的 bind 开始读这个表达式,它告诉我们将绑定到成员 map_type::value_type::second (它是一个 ptr_type),然后我们再绑定成员函数 ptr_type::get (它返回 shared_ptr 的指向物),最后我们对整个表达式执行 operator!. 由于指针可以隐式转换为 bool, 因此这是一个有效的布尔表达式。看完条件部分,我们来看 then-部分。
bind(&map_type::value_type::second,_1)= bind(constructor<ptr_type>(), bind(new_ptr<derived>())),
这里有三个 bind 表达式,第一个(我们从最左边开始,因为这个表达式有一个赋值)取出成员 map_type::value_type::second, 它是一个智能指针。这就是我们要赋给它一个新的 derived 的那个值。第二和第三个表达式是嵌套的,所以我们从里向外读。里面的 bind 负责堆上的一个 derived 实例的缺省构造,我们再在它的结果之上 bind 一个对 ptr_type (智能指针类型)的 constructor 的调用,然后把它的结果赋值(使用普通的赋值符)给最先的那个 bind 表达式。然后,我们给这个 then-部分再加一个表达式,打印出一个简短的信息和元素的键值。
var(std::cout) << "Created a new derived for \"" << bind(&map_type::value_type::first,_1) << "\".\n")
最后,我们加上语句的 else-部分,它打印出元素的键值和一些文字。
var(std::cout) << "\"" << bind(&map_type::value_type::first,_1) << "\" already has a valid pointer.\n"));
把这个表达式分开来读,很明显它们并不是那么复杂,虽然整个看起来很可怕。很重要的一点是,缩进和分离这些代码可以更容易阅读。我们可以写出类似的表达式来完成这件工作,它与这一个版本非常不同,更难阅读,但是它的效率稍高一些。这里要注意的是,通常都会有好几种方法来写 lambda 表达式,就象其它编程问题的情况一样。在写代码之前应该多想一下,因为你的选择会影响最终结果的可读性。作为比较,以下是我提到的另一个版本:
std::for_each(m.begin(),m.end(), if_then_else(!bind(&map_type::value_type::second,_1), ((bind(static_cast<void (ptr_type::*)(base*)> (&ptr_type::reset<base>), bind(&map_type::value_type::second,_1), bind(new_ptr<derived>()))), var(std::cout) << "Created a new derived for \"" << bind(&map_type::value_type::first,_1) << "\".\n"), var(std::cout) << "\"" << bind(&map_type::value_type::first,_1) << "\" already has a valid pointer.\n"));
这不是好的代码,这些代码由于类型转换和复杂的嵌套 binds 而变得混乱,与前面那个版本相比,这个版本很容易使我们偏离主要逻辑。为了弄明白它,我们再来把这个表达式切开成几个部分。首先,我们有条件部分,它很简单(这个表达式中没有其它东西!);我们利用对 shared_ptr 的了解,它告诉我们有一个到 bool 的隐式转换可用。因此我们可以去掉在前一个版本中使用的到成员函数 get 的 bind 。
!bind(&map_type::value_type::second,_1)
这个条件部分与原先的表达式一样工作。接下来的部分是:
bind(static_cast<void (ptr_type::*)(base*)> (&ptr_type::reset<base>), bind(&map_type::value_type::second,_1), bind(new_ptr<derived>()))
这真的很难分析,所以我们先避开它。我们不使用赋值,而是直接使用成员函数 reset, 它不仅是泛化的而且还是重载的。因此我们需要执行 static_cast 来告诉编译器我们要用那个版本的 reset 。这里主要是 static_cast 让表达式变得复杂,但是从最里面的表达式开始分析,我们就可以弄懂它。我们绑定一个调用到 operator new, 创建一个 derived 实例,然后我们把结果绑定到智能指针(通过成员 map_type::value_type::second), 然后再绑定到 shared_ptr 的成员函数 reset. 结果是,对元素中的智能指针调用 reset ,参数是一个新构造的 derived 实例。虽然我们在前一个例子中也完成了同样的工作,但这个版本更难以理解。
要记住,通常有一些因素让 lambda 表达式更容易或者更难阅读和理解,要认真考虑这些因素并尽可能选择容易的形式。这对于获得这个库所提供的能力是必要的,而且它会影响到你后面的程序员。
抛出及捕获异常
我们已经来到本章的最后一节,讨论 lambda 表达式中的异常处理。如果你对这个话题的反应是怀疑在 lambda 表达式中是否需要异常处理,这就和我的第一感觉一样了。但是,这可能还不是你的想法。你写过在处理数据的循环中执行局部异常处理的代码吗?是的,手写的循环可以避免使用 Boost.Lambda 库,因此把异常处理放入 lambda 表达式是很自然的。
要使用 Boost.Lambda 的异常处理工具,就要包含头文件 "boost/lambda/exceptions.hpp". 我们来重用一下前面看到的类 base 和 derived ,并象我们前面做过的那样执行 dynamic_cast ,不过这次我们要对引用执行转型而不是对指针,这意味着如果失败的话,dynamic_cast 将抛出一个异常。这使得这个例子比前面那个更直观,因为我们不需要再使用 if 语句。
#include <iostream> #include "boost/lambda/lambda.hpp" #include "boost/lambda/casts.hpp" #include "boost/lambda/bind.hpp" #include "boost/lambda/exceptions.hpp" int main() { using namespace boost::lambda; base* p1=new base; base* p2=new derived; (try_catch( bind(&derived::do_more_stuff,ll_dynamic_cast<derived&>(*_1)), catch_exception<std::bad_cast>(bind(&base::do_stuff,_1))))(p1); (try_catch( bind(&derived::do_more_stuff, ll_dynamic_cast<derived&>(*_1)), catch_exception<std::bad_cast>( bind(&base::do_stuff,_1))))(p2); }
这些表达式示范了把一个表达式包装到一个对 try_catch 的调用。try_catch 的通常形式为:
try_catch(expression, catch_exception<T1>(expression), catch_exception<T2>(expression, catch_all(expression))
在这段例子代码中,表达式对 derived& 使用 dynamic_cast。第一个转型由于 p1 指向一个 base 实例而失败;第二个转型则由于 p2 指向一个 derived 实例而成功。请留意对占位符的解引用(*_1)。这是必须的,因为我们是传送指针参数给表达式的,而 dynamic_cast 要求的是对象或引用。如果你需要 try_catch 处理几种类型的异常,就要确保把最特化的类型放在前面,就象普通的异常处理代码一样。[9]
9] 否则,一个更为通用的类型将会匹配该异常而不能查找到更为特殊的类型。具体详情请参考你喜欢的C++书籍。
如果我们想访问捕获的异常,可以用一个特别的占位符,_e. 当然,在 catch_all 中不能这样做,就象在 catch (...) 中没有异常对象一样。继续前面的例子,我们可以打印出令 dynamic_cast 失败的原因,象这样:
try_catch( bind(&derived::do_more_stuff,ll_dynamic_cast<derived&>(*_1)), catch_exception<std::bad_cast> (std::cout << bind(&std::exception::what,_e))))(p1);
在处理一个派生自 std::exception 的异常类型时——这是很常见的情形——你可以绑定到虚拟成员函数 what, 就象这里所示范的那样。
但是有时候,你不想捕获异常,而是想抛出一个异常。这可以通过函数 throw_exception 来实现。因为你需要创建一个异常对象用来抛出,所以从一个 lambda 表达式中抛出异常通常还要使用 constructor 。下面这个例子定义了一个异常类,some_exception, 它公有派生自 std::exception, 如果 lambda 表达式的参数为 true ,就创建并抛出一个异常。
#include <iostream> #include <exception> #include "boost/lambda/lambda.hpp" #include "boost/lambda/exceptions.hpp" #include "boost/lambda/if.hpp" #include "boost/lambda/construct.hpp" #include "boost/lambda/bind.hpp" class some_exception : public std::exception { std::string what_; public: some_exception(const char* what) : what_(what) {} virtual const char* what() const throw() { return what_.c_str(); } virtual ~some_exception() throw() {} }; int main() { using namespace boost::lambda; try { std::cout << "Throw an exception here.\n"; (if_then(_1==true,throw_exception( bind(constructor<some_exception>(), constant("Somewhere, something went \ terribly wrong.")))))(make_const(true)); std::cout << "We'll never get here!\n"; } catch(some_exception& e) { std::cout << "Caught exception, \"" << e.what() << "\"\n"; } }
运行这段程序将产生以下输出:
Throw an exception here. Caught exception, "Somewhere, something went terribly wrong."
最有趣的地方是抛出异常的那段代码。
throw_exception( bind(constructor<some_exception>(), constant("Somewhere, something went \ terribly wrong."))
throw_exception 的参数是一个 lambda 表达式。这个例子中,它被创建并绑定到对 some_exception 构造函数的调用,我们把一个字符串作为 what 参数传给该构造函数。
这就是在 Boost.Lambda 中进行异常处理的全部内容。永远记住,要小心谨慎地使用这些工具,它们可以让生活更轻松或更困难,这取决于你能否很好地并明智地使用它们[10] 。抛出并处理异常在你的 lambda 表达式中并不常见,但有些时候还是需要的。
[10] 小心应了这句格言,"如果你只有锤子,那么所有东西看起来都象钉子"。