本节详细描述了 lambda 表达式的不同类别。我们为每种可能的 lambda 表达式形式都单独用一节进行介绍。
BLL 定义了三种占位符类型:placeholder1_type
、placeholder2_type
和 placeholder3_type
。BLL 为每种占位符类型预定义了一个占位符变量:_1
、_2
和 _3
。但是,用户并非必须使用这些占位符。可以很容易地定义具有替代名称的占位符。这通过定义占位符类型的新变量来完成。例如
boost::lambda::placeholder1_type X; boost::lambda::placeholder2_type Y; boost::lambda::placeholder3_type Z;
定义了这些变量后,X += Y * Z
等价于 _1 += _2 * _3
。
lambda 表达式中占位符的使用决定了结果函数是零元、一元、二元还是三元函数。最高的占位符索引起决定作用。例如
_1 + 5 // unary _1 * _1 + _1 // unary _1 + _2 // binary bind(f, _1, _2, _3) // 3-ary _3 + 10 // 3-ary
请注意,最后一行创建了一个三元函数,该函数将其第三个参数加上 10
。前两个参数被丢弃。此外,lambda 仿函数只具有最小元数。总是可以提供比实际需要更多的参数(最多为支持的占位符数量)。剩余的参数将被丢弃。例如
int i, j, k; _1(i, j, k) // returns i, discards j and k (_2 + _2)(i, j, k) // returns j+j, discards i and k
有关此功能背后的设计原理,请参阅 “ Lambda 仿函数元数 ” 一节。
除了这三种占位符类型之外,还有第四种占位符类型 placeholderE_type
。此占位符的用法在 “异常” 一节中定义,该节描述了 lambda 表达式中的异常处理。
当为占位符提供实际参数时,参数传递模式始终是按引用传递。这意味着对占位符的任何副作用都将反映到实际参数中。例如
int i = 1; (_1 += 2)(i); // i is now 3 (++_1, cout << _1)(i) // i is now 4, outputs 4
基本规则是,任何 C++ 运算符调用,只要其中至少一个参数是 lambda 表达式,则其本身也是 lambda 表达式。几乎所有可重载的运算符都受支持。例如,以下是一个有效的 lambda 表达式
cout << _1, _2[_3] = _1 && false
但是,有一些限制源于 C++ 运算符重载规则,以及一些特殊情况。
有些运算符根本无法重载(::
、.
、.*
)。对于某些运算符,对返回类型的要求阻止了它们被重载以创建 lambda 仿函数。这些运算符是 ->.
、->
、new
、new[]
、delete
、delete[]
和 ?:
(条件运算符)。
这些运算符必须作为类成员来实现。因此,左操作数必须是 lambda 表达式。例如
int i; _1 = i; // ok i = _1; // not ok. i is not a lambda expression
对于此限制,有一个简单的解决方案,在 “延迟常量和变量” 一节中进行了描述。简而言之,可以通过用特殊的 var
函数包装左侧参数,将其显式转换为 lambda 仿函数
var(i) = _1; // ok
逗号运算符是 lambda 表达式中的“语句分隔符”。由于逗号也是函数调用中参数之间的分隔符,因此有时需要额外的括号
for_each(a.begin(), a.end(), (++_1, cout << _1));
如果没有 ++_1, cout << _1
周围的额外括号,则代码将被解释为尝试使用四个参数调用 for_each
。
逗号运算符创建的 lambda 仿函数遵循 C++ 规则,即始终先计算左操作数,然后再计算右操作数。在上面的示例中,a
的每个元素首先递增,然后写入流。
成员指针运算符 operator->*
可以自由重载。因此,对于用户定义的类型,成员指针运算符不是特殊情况。但是,内置含义的情况稍微复杂一些。如果左侧参数是指向某个类 A
的对象的指针,并且右侧参数是指向 A
的成员的指针,或者是指向 A
派生自的类的成员的指针,则应用内置成员指针运算符。我们必须区分两种情况
右侧参数是指向数据成员的指针。在这种情况下,lambda 仿函数只是执行参数替换,并调用内置成员指针运算符,该运算符返回对指向的成员的引用。例如
struct A { int d; }; A* a = new A(); ... (a ->* &A::d); // returns a reference to a->d (_1 ->* &A::d)(a); // likewise
右侧参数是指向成员函数的指针。对于这样的内置调用,结果有点像延迟成员函数调用。这样的表达式必须后跟一个函数参数列表,用于执行延迟成员函数调用。例如
struct B { int foo(int); }; B* b = new B(); ... (b ->* &B::foo) // returns a delayed call to b->foo // a function argument list must follow (b ->* &B::foo)(1) // ok, calls b->foo(1) (_1 ->* &B::foo)(b); // returns a delayed call to b->foo, // no effect as such (_1 ->* &B::foo)(b)(1); // calls b->foo(1)
绑定表达式可以有两种形式
bind(target-function
,bind-argument-list
) bind(target-member-function
,object-argument
,bind-argument-list
)
绑定表达式会延迟函数的调用。如果此目标函数是 n 元的,则 bind-argument-list
也必须包含 n 个参数。在当前版本的 BLL 中,0 <= n <= 9 必须成立。对于成员函数,参数数量最多为 8 个,因为对象参数占用一个参数位置。基本上,bind-argument-list
必须是目标函数的有效参数列表,只是任何参数都可以替换为占位符,或者更一般地,替换为 lambda 表达式。请注意,目标函数也可以是 lambda 表达式。绑定表达式的结果是零元、一元、二元或三元函数对象,具体取决于 bind-argument-list
中占位符的使用情况(请参阅 “占位符” 一节)。
绑定表达式创建的 lambda 仿函数的返回类型可以作为显式指定的模板参数给出,如下例所示
bind<RET>(target-function, bind-argument-list)
只有在无法推导出目标函数的返回类型时,这才必要。
以下各节描述了绑定表达式的不同类型。
目标函数可以是函数的指针或引用,并且可以是绑定或未绑定的。例如
X foo(A, B, C); A a; B b; C c; bind(foo, _1, _2, c)(a, b); bind(&foo, _1, _2, c)(a, b); bind(_1, a, b, c)(foo);
使用这种类型的绑定表达式,返回类型推导总是成功。
请注意,在 C++ 中,只有在地址分配给变量或用作变量的初始化器时,才有可能获取重载函数的地址,变量的类型解决了歧义,或者如果使用了显式强制转换表达式。这意味着重载函数不能直接在绑定表达式中使用,例如
void foo(int); void foo(float); int i; ... bind(&foo, _1)(i); // error ... void (*pf1)(int) = &foo; bind(pf1, _1)(i); // ok bind(static_cast<void(*)(int)>(&foo), _1)(i); // ok
在绑定表达式中使用成员函数指针的语法是
bind(target-member-function
,object-argument
,bind-argument-list
)
对象参数可以是对象的引用或指针,BLL 以统一的接口支持这两种情况
bool A::foo(int) const; A a; vector<int> ints; ... find_if(ints.begin(), ints.end(), bind(&A::foo, a, _1)); find_if(ints.begin(), ints.end(), bind(&A::foo, &a, _1));
同样,如果对象参数是未绑定的,则可以通过指针或引用调用结果 lambda 仿函数
bool A::foo(int); list<A> refs; list<A*> pointers; ... find_if(refs.begin(), refs.end(), bind(&A::foo, _1, 1)); find_if(pointers.begin(), pointers.end(), bind(&A::foo, _1, 1));
即使接口相同,使用指针或引用作为对象参数也存在重要的语义差异。差异源于 bind
函数获取其参数的方式,以及绑定参数在 lambda 仿函数中的存储方式。对象参数具有与任何其他绑定参数槽相同的参数传递和存储机制(请参阅 “在 lambda 函数中存储绑定参数” 一节);它作为 const 引用传递,并在 lambda 仿函数中存储为 const 副本。这在 lambda 仿函数和原始成员函数之间,以及看似相似的 lambda 仿函数之间产生了一些不对称性。例如
class A { int i; mutable int j; public: A(int ii, int jj) : i(ii), j(jj) {}; void set_i(int x) { i = x; }; void set_j(int x) const { j = x; }; };
当使用指针时,行为是程序员可能期望的
A a(0,0); int k = 1; bind(&A::set_i, &a, _1)(k); // a.i == 1 bind(&A::set_j, &a, _1)(k); // a.j == 1
即使存储了对象参数的 const 副本,原始对象 a
仍然会被修改。这是因为对象参数是指针,并且复制的是指针,而不是指针指向的对象。当我们使用引用时,行为有所不同
A a(0,0); int k = 1; bind(&A::set_i, a, _1)(k); // error; a const copy of a is stored. // Cannot call a non-const function set_i bind(&A::set_j, a, _1)(k); // a.j == 0, as a copy of a is modified
为了防止发生复制,可以使用 ref
或 cref
包装器(var
和 constant_ref
也可以)
bind(&A::set_i, ref(a), _1)(k); // a.j == 1 bind(&A::set_j, cref(a), _1)(k); // a.j == 1
请注意,前面的讨论仅与绑定参数相关。如果对象参数是未绑定的,则参数传递模式始终是按引用传递。因此,在下面两个 lambda 仿函数的调用中,不会复制参数 a
A a(0,0); bind(&A::set_i, _1, 1)(a); // a.i == 1 bind(&A::set_j, _1, 1)(a); // a.j == 1
指向成员变量的指针实际上不是函数,但是 bind
函数的第一个参数仍然可以是成员变量的指针。调用这样的绑定表达式会返回对数据成员的引用。例如
struct A { int data; }; A a; bind(&A::data, _1)(a) = 1; // a.data == 1
访问其成员的对象的 cv 限定符受到尊重。例如,以下代码尝试写入 const 位置
const A ca = a; bind(&A::data, _1)(ca) = 1; // error
函数对象,即定义了函数调用运算符的类对象,可以用作目标函数。一般来说,BLL 无法推导出任意函数对象的返回类型。但是,有两种方法可以为 BLL 提供针对特定函数对象类的这种能力。
BLL 支持标准库约定,即在函数对象类中使用名为 result_type
的成员 typedef 声明函数对象的返回类型。这是一个简单的例子
struct A { typedef B result_type; B operator()(X, Y, Z); };
如果函数对象未定义 result_type
typedef,则会尝试使用下面描述的方法(sig
模板)来解析函数对象的返回类型。如果函数对象同时定义了 result_type
和 sig
,则 result_type
优先。
使 BLL 了解函数对象返回类型(或多个返回类型)的另一种机制是定义成员模板结构 sig<Args>
,其中包含指定返回类型的 typedef type
。这是一个简单的例子
struct A { template <class Args> struct sig { typedef B type; } B operator()(X, Y, Z); };
模板参数 Args
是一个 tuple
(或更准确地说是一个 cons
列表)类型 [tuple],其中第一个元素是函数对象类型本身,其余元素是函数对象被调用时使用的参数类型。与定义 result_type
typedef 相比,这可能看起来过于复杂。但是,仅使用简单的 typedef 来表示返回类型存在两个重要的限制
如果函数对象定义了多个函数调用运算符,则无法为它们指定不同的返回类型。
如果函数调用运算符是模板,则返回类型可能取决于模板参数。因此,typedef 也应该是模板,但 C++ 语言不支持。
以下代码显示了一个示例,其中返回类型取决于其中一个参数的类型,以及如何使用 sig
模板表示该依赖关系
struct A { // the return type equals the third argument type: template<class T1, class T2, class T3> T3 operator()(const T1& t1, const T2& t2, const T3& t3) const; template <class Args> class sig { // get the third argument type (4th element) typedef typename boost::tuples::element<3, Args>::type T3; public: typedef typename boost::remove_cv<T3>::type type; }; };
Args
元组的元素始终是非引用类型。此外,元素类型可以具有 const 或 volatile 限定符(统称为 cv 限定符),或两者都有。这是因为参数中的 cv 限定符会影响返回类型。将潜在的 cv 限定函数对象类型本身包含到 Args
元组中的原因是,函数对象类可以同时包含 const 和 non-const(或 volatile,甚至 const volatile)函数调用运算符,并且它们可以各自具有不同的返回类型。
sig
模板可以看作是一个元函数,它将参数类型元组映射到使用元组中类型的参数进行的调用的结果类型。正如上面的示例所示,模板最终可能会变得有些复杂。要执行的典型任务是从元组中提取相关类型、删除 cv 限定符等。请参阅 Boost type_traits [type_traits] 和 Tuple [type_traits] 库,以获取可以帮助完成这些任务的工具。sig
模板是 FC++ 库 [fc++] 中首次引入的类似机制的改进版本。
返回类型推导系统可能无法推导出某些用户定义的运算符或带有类对象的绑定表达式的返回类型。提供了一种特殊的 lambda 表达式类型,用于显式声明返回类型并重写推导系统。要声明由 lambda 表达式 e
定义的 lambda 仿函数的返回类型为 T
,您可以编写
ret<T>(e);
效果是根本不为 lambda 表达式 e
执行返回类型推导,而是使用 T
作为返回类型。显然,T
不能是任意类型,lambda 仿函数的真实结果必须可以隐式转换为 T
。例如
A a; B b; C operator+(A, B); int operator*(A, B); ... ret<D>(_1 + _2)(a, b); // error (C cannot be converted to D) ret<C>(_1 + _2)(a, b); // ok ret<float>(_1 * _2)(a, b); // ok (int can be converted to float) ... struct X { Y operator(int)(); }; ... X x; int i; bind(x, _1)(i); // error, return type cannot be deduced ret<Y>(bind(x, _1))(i); // ok
对于绑定表达式,可以使用速记符号来代替 ret
。最后一行也可以写成
bind<Z>(x, _1)(i);
此功能是仿照 Boost Bind 库 [bind] 建模的。
请注意,在嵌套的 lambda 表达式中,必须在每个子表达式中使用 ret
,否则推导会失败。例如
A a; B b; C operator+(A, B); D operator-(C); ... ret<D>( - (_1 + _2))(a, b); // error ret<D>( - ret<C>(_1 + _2))(a, b); // ok
如果您发现自己反复使用具有相同类型的 ret
,则值得扩展返回类型推导(请参阅 “扩展返回类型推导系统” 一节)。
如上所述,ret
的作用是阻止执行返回类型推导。但是,有一个例外。由于 C++ 模板实例化的工作方式,编译器始终被迫为零参数 lambda 仿函数实例化返回类型推导模板。这给 ret
带来了一个小问题,最好用一个例子来描述
struct F { int operator()(int i) const; }; F f; ... bind(f, _1); // fails, cannot deduce the return type ret<int>(bind(f, _1)); // ok ... bind(f, 1); // fails, cannot deduce the return type ret<int>(bind(f, 1)); // fails as well!
BLL 无法推导出上述绑定调用的返回类型,因为 F
未定义 typedef result_type
。人们可能期望 ret
能够解决此问题,但对于从绑定表达式产生的零元 lambda 仿函数(上面的最后一行),这不起作用。返回类型推导模板被实例化,即使这不是必要的,结果也会导致编译错误。
解决此问题的方法不是使用 ret
函数,而是在 bind
调用中将返回类型定义为显式指定的模板参数
bind<int>(f, 1); // ok
用 ret<
和 T
>(bind(arg-list
))bind<
创建的 lambda 仿函数具有完全相同的功能 — 除了对于某些零元 lambda 仿函数,前者不起作用而后者起作用之外。T
>(arg-list
)
一元函数 constant
、constant_ref
和 var
将它们的参数转换为 lambda 仿函数,该仿函数实现恒等映射。前两个用于常量,后一个用于变量。由于缺少 lambda 表达式的显式语法,因此有时需要使用这些延迟常量和变量。例如
for_each(a.begin(), a.end(), cout << _1 << ' '); for_each(a.begin(), a.end(), cout << ' ' << _1);
第一行输出 a
的元素,并用空格分隔,而第二行输出一个空格,后跟 a
的元素,没有任何分隔符。原因是 cout << ' '
的操作数都不是 lambda 表达式,因此 cout << ' '
会立即求值。为了延迟 cout << ' '
的求值,必须将其中一个操作数显式标记为 lambda 表达式。这可以通过 constant
函数来完成
for_each(a.begin(), a.end(), cout << constant(' ') << _1);
调用 constant(' ')
会创建一个零元 lambda 仿函数,该仿函数存储字符常量 ' '
并在调用时返回对它的引用。constant_ref
函数类似,只是它存储对其参数的常量引用。只有当运算符调用具有副作用时,才需要 constant
和 consant_ref
,如上面的示例所示。
有时我们需要延迟变量的求值。假设我们想在编号列表中输出容器的元素
int index = 0; for_each(a.begin(), a.end(), cout << ++index << ':' << _1 << '\n'); for_each(a.begin(), a.end(), cout << ++var(index) << ':' << _1 << '\n');
第一个 for_each
调用没有达到我们想要的效果;index
只递增一次,并且其值只写入输出流一次。通过使用 var
使 index
成为 lambda 表达式,我们获得了期望的效果。
总而言之,var(x)
创建一个零元 lambda 仿函数,该仿函数存储对变量 x
的引用。当调用 lambda 仿函数时,会返回对 x
的引用。
可以在 lambda 表达式外部预定义和命名延迟变量或常量。模板 var_type
、constant_type
和 constant_ref_type
用于此目的。它们的用法如下
var_type<T>::type delayed_i(var(i)); constant_type<T>::type delayed_c(constant(c));
第一行定义了变量 delayed_i
,它是类型为 T
的变量 i
的延迟版本。类似地,第二行定义了常量 delayed_c
,它是常量 c
的延迟版本。例如
int i = 0; int j; for_each(a.begin(), a.end(), (var(j) = _1, _1 = var(i), var(i) = var(j)));
等价于
int i = 0; int j; var_type<int>::type vi(var(i)), vj(var(j)); for_each(a.begin(), a.end(), (vj = _1, _1 = vi, vi = vj));
这是一个命名延迟常量的示例
constant_type<char>::type space(constant(' ')); for_each(a.begin(),a.end(), cout << space << _1);
如 “赋值和下标运算符” 一节中所述,赋值和下标运算符始终定义为成员函数。这意味着,对于形式为 x = y
或 x[y]
的表达式要解释为 lambda 表达式,左侧操作数 x
必须是 lambda 表达式。因此,有时有必要为此目的使用 var
。我们重复 “赋值和下标运算符” 一节中的示例
int i; i = _1; // error var(i) = _1; // ok
请注意,复合赋值运算符 +=
、-=
等可以定义为非成员函数,因此即使只有右侧操作数是 lambda 表达式,它们也会被解释为 lambda 表达式。尽管如此,显式延迟左操作数是完全可以的。例如,i += _1
等价于 var(i) += _1
。
BLL 定义了几个函数来创建表示控制结构的 lambda 仿函数。它们都将 lambda 仿函数作为参数并返回 void
。首先举一个例子,以下代码输出某个容器 a
的所有偶数元素
for_each(a.begin(), a.end(), if_then(_1 % 2 == 0, cout << _1));
BLL 支持以下用于控制结构的函数模板
if_then(condition, then_part) if_then_else(condition, then_part, else_part) if_then_else_return(condition, then_part, else_part) while_loop(condition, body) while_loop(condition) // no body case do_while_loop(condition, body) do_while_loop(condition) // no body case for_loop(init, condition, increment, body) for_loop(init, condition, increment) // no body case switch_statement(...)
所有控制结构 lambda 仿函数的返回类型都是 void
,但 if_then_else_return
除外,它包装了对条件运算符的调用
condition ? then_part : else_part
此运算符的返回类型规则有些复杂。基本上,如果分支具有相同的类型,则此类型就是返回类型。如果分支的类型不同,则一个分支(例如类型 A
)必须可转换为另一个分支(例如类型 B
)。在这种情况下,结果类型为 B
。此外,如果公共类型是左值,则返回类型也将是左值。
延迟变量在控制结构 lambda 表达式中往往很常见。例如,在这里,我们使用 var
函数将 for_loop
的参数转换为 lambda 表达式。代码的效果是将二维数组的每个元素加 1
int a[5][10]; int i; for_each(a, a+5, for_loop(var(i)=0, var(i)<10, ++var(i), _1[var(i)] += 1));
BLL 支持 Joel de Guzmann 建议的控制表达式的替代语法。通过重载 operator[]
,我们可以获得与内置控制结构更接近的相似性
if_(condition)[then_part] if_(condition)[then_part].else_[else_part] while_(condition)[body] do_[body].while_(condition) for_(init, condition, increment)[body]
例如,使用此语法,上面的 if_then
示例可以写成
for_each(a.begin(), a.end(), if_(_1 % 2 == 0)[ cout << _1 ])
随着获得更多经验,我们最终可能会弃用这些语法中的一种或另一种。
用于 switch
控制结构的 lambda 表达式更复杂,因为 case 的数量可能会有所不同。switch lambda 表达式的一般形式是
switch_statement(condition
, case_statement<label
>(lambda expression
), case_statement<label
>(lambda expression
), ... default_statement(lambda expression
) )
参数必须是一个 lambda 表达式,它创建一个具有整型返回类型的 lambda 仿函数。不同的 case 是用 condition
case_statement
函数创建的,可选的 default case 是用 default_statement
函数创建的。case 标签作为显式指定的模板参数提供给 case_statement
函数,并且 break
语句是每个 case 的隐式部分。例如,case_statement<1>(a)
,其中 a
是某个 lambda 仿函数,将生成以下代码
case 1:
evaluate lambda functor
a;
break;
switch_statement
函数专门用于最多 9 个 case 语句。
作为一个具体的示例,以下代码迭代某个容器 v
,并为每个 0
输出 “zero”,为每个 1
输出 “one”,对于任何其他值 n
输出 “other: n
”。请注意,另一个 lambda 表达式在 switch_statement
之后排序,以便在每个元素之后输出换行符
std::for_each(v.begin(), v.end(), ( switch_statement( _1, case_statement<0>(std::cout << constant("zero")), case_statement<1>(std::cout << constant("one")), default_statement(cout << constant("other: ") << _1) ), cout << constant("\n") ) );
BLL 提供了抛出和捕获异常的 lambda 仿函数。用于抛出异常的 lambda 仿函数是使用一元函数 throw_exception
创建的。此函数的参数是要抛出的异常,或创建要抛出的异常的 lambda 仿函数。用于重新抛出异常的 lambda 仿函数是使用零元 rethrow
函数创建的。
用于处理异常的 Lambda 表达式稍微复杂一些。try catch 块的 lambda 表达式的一般形式如下
try_catch(lambda expression
, catch_exception<type
>(lambda expression
), catch_exception<type
>(lambda expression
), ... catch_all(lambda expression
) )
第一个 lambda 表达式是 try 块。每个 catch_exception
定义一个 catch 块,其中显式指定的模板参数定义要捕获的异常类型。catch_exception
中的 lambda 表达式定义了如果捕获到异常要采取的操作。请注意,结果异常处理程序将异常作为引用捕获,即 catch_exception<T>(...)
会产生 catch 块
catch(T& e) { ... }
最后一个 catch 块也可以是对 catch_exception<
或 type
>catch_all
的调用,后者是等效于 catch(...)
的 lambda 表达式。
示例 17.1,“在 lambda 表达式中抛出和处理异常。” 演示了 BLL 异常处理工具的用法。第一个处理程序捕获 foo_exception
类型的异常。请注意处理程序主体中 _1
占位符的用法。
第二个处理程序展示了如何抛出异常,并演示了异常占位符 _e
的用法。它是一个特殊的占位符,在处理程序主体中引用捕获的异常对象。这里我们正在处理 std::exception
类型的异常,该异常携带一个字符串,用于解释异常的原因。可以使用零参数成员函数 what
查询此解释。表达式 bind(&std::exception::what, _e)
创建了用于进行该调用的 lambda 函数。请注意,_e
不能在异常处理程序 lambda 表达式之外使用。第二个处理程序的最后一行构造了一个新的异常对象,并使用 throw exception
抛出该对象。在 lambda 表达式中构造和析构对象在 “构造和析构” 一节中进行了解释
最后,第三个处理程序(catch_all
)演示了重新抛出异常。
示例 17.1. 在 lambda 表达式中抛出和处理异常。
for_each( a.begin(), a.end(), try_catch( bind(foo, _1), // foo may throw catch_exception<foo_exception>( cout << constant("Caught foo_exception: ") << "foo was called with argument = " << _1 ), catch_exception<std::exception>( cout << constant("Caught std::exception: ") << bind(&std::exception::what, _e), throw_exception(bind(constructor<bar_exception>(), _1))) ), catch_all( (cout << constant("Unknown"), rethrow()) ) ) );
运算符 new
和 delete
可以重载,但它们的返回类型是固定的。特别是,返回类型不能是 lambda 仿函数,这阻止了它们被重载用于 lambda 表达式。不可能获取构造函数的地址,因此构造函数不能用作绑定表达式中的目标函数。析构函数也是如此。作为绕过这些约束的一种方法,BLL 为 new
和 delete
调用以及构造函数和析构函数定义了包装器类。这些类的实例是函数对象,可以用作绑定表达式的目标函数。例如
int* a[10]; for_each(a, a+10, _1 = bind(new_ptr<int>())); for_each(a, a+10, bind(delete_ptr(), _1));
new_ptr<int>()
表达式创建一个函数对象,该对象在调用时调用 new int()
,并在 bind
内部包装该对象使其成为 lambda 仿函数。以相同的方式,表达式 delete_ptr()
创建一个函数对象,该对象在其参数上调用 delete
。请注意,new_ptr<
也可以接受参数。它们直接传递给构造函数调用,因此允许调用接受参数的构造函数。T
>()
作为 lambda 表达式中构造函数调用的示例,以下代码从两个容器 x
和 y
中读取整数,从中构造 pair,并将它们插入到第三个容器中
vector<pair<int, int> > v; transform(x.begin(), x.end(), y.begin(), back_inserter(v), bind(constructor<pair<int, int> >(), _1, _2));
表 17.1,“与构造和析构相关的函数对象。” 列出了所有与创建和销毁对象相关的函数对象,显示了创建和调用函数对象的表达式,以及计算该表达式的效果。
表 17.1. 与构造和析构相关的函数对象。
函数对象调用 | 包装的表达式 |
---|---|
constructor<T>()( |
T(arg_list ) |
destructor()(a) |
a.~A() ,其中 a 的类型为 A |
destructor()(pa) |
pa->~A() ,其中 pa 的类型为 A* |
new_ptr<T>()( |
new T( |
new_array<T>()(sz) |
new T[sz] |
delete_ptr()(p) |
delete p |
delete_array()(p) |
delete p[] |
当调用 lambda 仿函数时,默认行为是在所有子表达式中将实际参数替换为占位符。本节介绍了防止子表达式的替换和求值的工具,并解释了何时应使用这些工具。
绑定表达式的参数可以是任意 lambda 表达式,例如,其他绑定表达式。例如
int foo(int); int bar(int); ... int i; bind(foo, bind(bar, _1))(i);
最后一行进行调用 foo(bar(i));
请注意,绑定表达式中的第一个参数(目标函数)也不例外,因此也可以是绑定表达式。最内层的 lambda 仿函数只需要返回可以用作目标函数的东西:另一个 lambda 仿函数、函数指针、成员函数指针等。例如,在以下代码中,最内层的 lambda 仿函数在两个函数之间进行选择,并返回指向其中一个函数的指针
int add(int a, int b) { return a+b; } int mul(int a, int b) { return a*b; } int(*)(int, int) add_or_mul(bool x) { return x ? add : mul; } bool condition; int i; int j; ... bind(bind(&add_or_mul, _1), _2, _3)(condition, i, j);
如果目标函数是一个类型取决于模板参数的变量,则可能会意外发生嵌套的绑定表达式。通常,目标函数可能是函数模板的形式参数。在这种情况下,程序员可能不知道目标函数是否是 lambda 仿函数。
考虑以下函数模板
template<class F> int nested(const F& f) { int x; ... bind(f, _1)(x); ... }
在该函数的某个位置,形式参数 f
在绑定表达式中用作目标函数。为了使此 bind
调用有效,f
必须是一元函数。假设对 nested
进行了以下两个调用
int foo(int); int bar(int, int); nested(&foo); nested(bind(bar, 1, _1));
两者都是一元函数或函数对象,具有适当的参数和返回类型,但后者将无法编译。在后一个调用中,nested
内部的绑定表达式将变为
bind(bind(bar, 1, _1), _1)
当使用 x
调用它时,在替换之后,我们最终尝试调用
bar(1, x)(x)
这是一个错误。bar
的调用返回 int,而不是一元函数或函数对象。
在上面的示例中,nested
函数中绑定表达式的意图是将 f
视为普通函数对象,而不是 lambda 仿函数。BLL 提供了函数模板 unlambda
来表达这一点:在 unlambda
内部包装的 lambda 仿函数不再是 lambda 仿函数,并且不参与参数替换过程。请注意,对于所有其他参数类型,unlambda
都是恒等运算,除了使非 const 对象变为 const。
使用 unlambda
,nested
函数可以这样写成
template<class F> int nested(const F& f) { int x; ... bind(unlambda(f), _1)(x); ... }
protect
函数与 unlambda 相关。它也用于防止参数替换的发生,但与 unlambda
将 lambda 仿函数永久转换为普通函数对象不同,protect
只是临时执行此操作,仅用于一个求值轮次。例如
int x = 1, y = 10; (_1 + protect(_1 + 2))(x)(y);
第一次调用将 x
替换为最左边的 _1
,并得到另一个 lambda 仿函数 x + (_1 + 2)
,在用 y
调用后,它变为 x + (y + 2)
,最终结果为 13。
将 protect
包含到库中的主要动机是为了允许嵌套的 STL 算法调用(名为 “嵌套 STL 算法调用” 的章节)。
lambda 仿函数的实际参数不能是非 const 右值。这是出于故意的设计决策:要么我们有这个限制,要么实际参数不能有副作用。有一些方法可以绕过这个限制。我们重复 名为 “关于 lambda 仿函数的实际参数” 的章节 中的示例,并列出不同的解决方案
int i = 1; int j = 2; (_1 + _2)(i, j); // ok (_1 + _2)(1, 2); // error (!)
如果右值是类类型,则创建右值的函数的返回类型应定义为 const。由于不幸的语言限制,这不适用于内置类型,因为内置右值不能被 const 限定。
如果 lambda 函数调用是可访问的,则可以使用 make_const
函数来 const 化 右值。例如
(_1 + _2)(make_const(1), make_const(2)); // ok
通常 lambda 函数调用站点位于标准算法函数模板内部,从而阻止使用此解决方案。
如果以上方法都不可行,则可以将 lambda 表达式包装在 const_parameters
函数中。它创建另一种类型的 lambda 仿函数,该仿函数将其参数作为 const 引用。例如
const_parameters(_1 + _2)(1, 2); // ok
请注意,const_parameters
使所有参数都变为 const。因此,如果其中一个参数是非 const 右值,而另一个参数需要作为非 const 引用传递,则不能使用此方法。
如果以上方法都不可行,仍然有一种解决方案,但不幸的是,它可能会破坏 const 正确性。该解决方案是另一个 lambda 仿函数包装器,我们将其命名为 break_const
,以提醒用户此函数的潜在危险。break_const
函数创建一个 lambda 仿函数,该仿函数将其参数作为 const,并在调用原始包装的 lambda 仿函数之前去除 const 性。例如
int i; ... (_1 += _2)(i, 2); // error, 2 is a non-const rvalue const_parameters(_1 += _2)(i, 2); // error, i becomes const break_const(_1 += _2)(i, 2); // ok, but dangerous
请注意,break_const
或 const_parameters
的结果不是 lambda 仿函数,因此它们不能用作 lambda 表达式的子表达式。例如
break_const(_1 + _2) + _3; // fails. const_parameters(_1 + _2) + _3; // fails.
但是,这种代码永远不应该需要,因为对子 lambda 仿函数的调用是在 BLL 内部进行的,并且不受非 const 右值问题的影响。
BLL 为四种类型转换表达式 static_cast
、dynamic_cast
、const_cast
和 reinterpret_cast
定义了对应的版本。BLL 版本的类型转换表达式具有前缀 ll_
。要转换成的类型作为显式指定的模板参数给出,唯一的参数是要执行转换的表达式。如果参数是 lambda 仿函数,则首先评估 lambda 仿函数。例如,以下代码使用 ll_dynamic_cast
来计算容器 a
中 derived
实例的数量
class base {}; class derived : public base {}; vector<base*> a; ... int count = 0; for_each(a.begin(), a.end(), if_then(ll_dynamic_cast<derived*>(_1), ++var(count)));
BLL 中这些表达式的对应版本分别命名为 ll_sizeof
和 ll_typeid
。两者都接受一个参数,该参数可以是 lambda 表达式。创建的 lambda 仿函数包装了 sizeof
或 typeid
调用,当调用 lambda 仿函数时,将执行包装的操作。例如
vector<base*> a; ... for_each(a.begin(), a.end(), cout << bind(&type_info::name, ll_typeid(*_1)));
这里 ll_typeid
创建一个 lambda 仿函数,用于为每个元素调用 typeid
。typeid
调用的结果是 type_info
类的实例,bind 表达式创建一个 lambda 仿函数,用于调用该类的 name
成员函数。
BLL 将常见的 STL 算法定义为函数对象类,其实例可以用作 bind 表达式中的目标函数。例如,以下代码迭代二维数组的元素,并计算它们的总和。
int a[100][200]; int sum = 0; std::for_each(a, a + 100, bind(ll::for_each(), _1, _1 + 200, protect(sum += _1)));
BLL 版本的 STL 算法是类,它们定义了函数调用运算符(或多个重载运算符)来调用 std
命名空间中的相应函数模板。所有这些结构都放在子命名空间 boost::lambda:ll
中。
请注意,没有简单的方法在 lambda 表达式中表达重载的成员函数调用。这限制了嵌套 STL 算法的实用性,例如 begin
函数在容器模板中有多个重载定义。一般来说,无法编写类似于以下伪代码的内容
std::for_each(a.begin(), a.end(), bind(ll::for_each(), _1.begin(), _1.end(), protect(sum += _1)));
不过,可以为常见的特殊情况提供一些帮助。BLL 定义了两个辅助函数对象类 call_begin
和 call_end
,它们分别包装对容器的 begin
和 end
函数的调用,并返回容器的 const_iterator
类型。使用这些辅助模板,上面的代码变为
std::for_each(a.begin(), a.end(), bind(ll::for_each(), bind(call_begin(), _1), bind(call_end(), _1), protect(sum += _1)));