译 / 李博(光宇广贞)
C++ 0x 中,“Lambda 表达式”隐式定义并构造匿名函数对像,比如下例“Hello World”的 Lambda 表达式:
图一
这个 [] 就是 Lambda 前导算符,它通知编译器其后引导的是一个 Lambda 表达式。( int n ) 是 Lambda 参数表声明。{ cout << n << “ “; } 是 Lambda 构造的匿名函数的函数体。还有一个隐含的声明是该匿名函数返回类型是 void。编译时,C++ 0x 将图一转化成为如图二的 C++ 98/03 的形式。通过图二对图一的转换便可以看到 Lambda 表达式的本质:
图二
Lambda 表达式的工作就是,定义匿名函数类并构造其对像,而后调用其括号算符重载函数。
注意图二中括号算符重载函数是 const 函数,要想构造一个 non-const 函数,则须在图一 Lambda 表达式的参数表 ( int n ) 后写上 mutable 关键字。接下来,表达式的函数定义体可以写成很多行,如图三:
图三
Lambda 表达式不止能定义返回类型为 void 的函数体。Lambda 表达式可以根据函数体中 return 表达式去推导返回类型,如图四:
图四
图四里 n * n * n 的类型是 int,因此该 Lambda 表达式的返回类型是 int。Lambda 表达式允许显式指定函数返回类型,如图五:
图五
图五中 –>double 便是 Lambda 表达式指定返回类型的方法。图四可以将之省略,而图五则必须指定。何时可以省略呢?当函数体仅有一句 return 表达式时是可以省略指定返回类型的(返回类型由编译器通过“decltype ( return expr )”推导);若不止有一句 return 表达式,而不指定返回类型的话,编译器则认为返回类型为 void。如图五代码,若将 –>double 去掉,则编译器会报错说:void 返回类型的函数体内不允许 return。
译者按:为何返回类型这里放在了参数表的右边,而不是像传统的 C 语言函数定义式一样,将返回类型放在参数表的左边呢?我曾在《C++ 0x 之 decltype 和 auto 受 VS 2010 支持》中讨论过,返回类型处在参数表左边带来的类型推导问题。
由于 C++ 语言在产生时便采用了 C 语言的代码风格,因此在函数定义上,返回类型处在参数表的左边。它的好外在于,函数待以右值,返回类型居前明确标明右值类型。然而,随着 C++ 的发展,C 语言的代码风格逐渐成了包袱,因此在新标准中,逐渐做了改变。
以上所枚举的 Lambda 定义的函数都是无状态的,因为没有包含数据成员,也就是说,Lambda 表达式并未与所在域的变量打交道。程序员可以定义有状态的 Lambda 表达式,使之“捕获”本地变量。“捕获”这个词用得很传神,在后面的示例中,会把这一概念逐步捋清。话说到这份儿上,应该点明的是 [] 算符表达的含义是“引导一个无状态的 Lambda 表达式”,因此,改变为有状态的表达式,便要从此入手。如图六:
图六
[] 里面多了 x 和 y,变成了 [x, y]。此即为“本地变量捕获表达式”。注意这里 Lambda 函数体内使用的 x 变量和 y 变量,也出现在 main 函数里。注意,此 xy 非彼 xy。那二者通过什么沟通呢?通过捕获表达式——本质上就是传参。按照图一到图二的形式,将图六还原成如图七的模样,便一目了然了:
图七
因此,若在 Lambda 表达式内使用 x 和 y 而未用捕获表达式捕获时,便会造成 Lambda 函数体内局部变量使用前未定义的错误。要明确:尽管 Lambda 表达式写在了 main 函数中,但是表达式内函数体的作用域是在 main 函数体外的。
由图六到图七可以看到,这种“传参”的方式是“传值”。若我想把凡是在 Lambda 表达式函数体中用到的本地变量都以值的形式传进去,若用得较多,那捕获表达式要写成一长串儿。有一个简便的方法,让 Lambda 表达式用 [=] 引导,如图八:
图八
如图八所示,Lambda 函数体中使用到的 x 和 y 变量在 [=] 的引导下自动从 main 函数中捕获,完成值传递。不过传值有着一系列的问题,比如临时对像啦、值拷贝啦、深复制啦、什么内(外)部更动不能外(内)传(原值语义,副作用问题)啦……等等。我想传引用怎么办?在捕获表达式中,对于要引用的本地变量前面加上 & 算符,比如图六的例子写成 [&x, &y] 便是捕获其引用。若想全部以引用形式捕获呢?使用 [&] 引导就行。注意传引用时,在构造 Lambda 函数对像的时候,C4512 号警告被忽略。如图九与图十:
图九
图十
注意到图十中在构造 LambdaFactor 类时把 C4512 号警告给“镇压”了。我们在图二和图七都没有看到这种情况,也就是说,只有在传引用的时候才会发生这种情况——因为这是由值向引用成员传递的过程引发的问题。
译者按:若类含有引用成员变量,而由外部变量传参在构造函数中对其初始化是失败的,编译不会报错,但是运行时行为未定义。类引用成员变量多用于引用类中的其它同型成员变量。而如图十所示,Lambda 表达式必须保证外部变量通过传参赋给类引用成员变量之后,对类引用成员变量的操作是行为正常的。因此在程序员使用 Lambda 表达式传引用时,C4512 号警告会被镇压。
若我有的想传值,有的想传引用呢?那就在捕获表达式里面把想传引用的变量前面标注 & 算符就好了。也有简便的办法,比如在图十一的例中,除了指定的本地变量传引用,其它一律传值,可以这样写:
图十一
在类成员函数中使用 Lambda 时,若表达式需要处理类成员变量,需要注意作用域的问题,比如图十二的用法是非法的:
图十二
看到报错了。错在哪儿呢?图二、图七、图十等例已经讲述了 Lambda 表达式的本质。类成员变量并非前例所述的本地变量。由于作用域是分离的,Lambda 对像不能直接访问类成员变量,必须通过 this 指针,也就是用 [this] 做前导。如图十三:
图十三
在传入 this 指针后,Lambda 表达式函数体内对 m_toys 成员的调用相当于 this->m_toys。图十三的例子中也可以使用 [=] 代表 [this],隐式传递 this 指针。但是 [&] 不行,因为没有 [&this] 的用法,注意,this 指针是右值,不能取值。
若 Lambda 表达式不需要传参,则可以把参数表给省略。比如 [&] { return i++; } 相当于 [&] () { return i++; }。到这里,综合 Lambda 表达式的一些特性,可以写出如下有意思的代码,如图十四:
图十四
图十四 14 和 15 行相当于定义了 20 行的函数,然后调用之。没有什么实际用,权当一笑。
总结一下。Lambda 表达式的成分如下,opt 表示可选项:
( lambda-param-list opt ) mutable opt exception-specification opt lambda-return-type opt
注意,在无参数表达式里,若需指定 mutable 关键字或者返回类型说明,则空括号 () 必须加上,不能省略。
若需要将 Lambda 表达式做为其它函数的参数,则使用 tr1::function 模板类对像做为形参接收 Lambda 表达式。注意 Lambda 表达式对像是常左值,如图十五:
图十五
文章所属博客分类:C++