Boost C++ 库

世界上最受推崇、设计最精良的 C++ 库项目之一。 Herb SutterAndrei AlexandrescuC++ 编码标准

Lambda 表达式详解 - Boost C++ 函数库
PrevUpHomeNext

Lambda 表达式详解

本节将详细介绍不同类别的 Lambda 表达式。我们为 Lambda 表达式的每种可能形式单独设置了一个章节。

占位符

BLL 定义了三种占位符类型:placeholder1_typeplaceholder2_typeplaceholder3_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

运算符表达式

基本规则是,任何至少有一个参数为 Lambda 表达式的 C++ 运算符调用本身就是一个 Lambda 表达式。几乎所有可重载的运算符都受支持。例如,以下是一个有效的 Lambda 表达式:

cout << _1, _2[_3] = _1 && false

然而,存在一些源于 C++ 运算符重载规则的限制,以及一些特殊情况。

无法重载的运算符

有些运算符根本无法重载(::..*)。对于某些运算符,对返回类型的要求阻止了它们被重载以创建 Lambda 仿函数。这些运算符是 ->.->newnew[]deletedelete[]?:(条件运算符)。

赋值和下标运算符

这些运算符必须作为类成员实现。因此,左操作数必须是一个 Lambda 表达式。例如:

int i; 
_1 = i;      // ok
i = _1;      // not ok. i is not a lambda expression

可以通过在 “延迟常量和变量” 一节中描述的简单方法来解决此限制。简而言之,可以通过用特殊的 var 函数将其包装起来,将左侧参数显式转换为 Lambda 仿函数。

var(i) = _1; // ok

逻辑运算符

逻辑运算符遵循短路求值规则。例如,在以下代码中,i 永远不会被递增:

bool flag = true; int i = 0;
(_1 || ++_2)(flag, i);

逗号运算符

逗号运算符是 Lambda 表达式中的“语句分隔符”。由于逗号也是函数调用中参数的分隔符,因此有时需要额外的括号。

for_each(a.begin(), a.end(), (++_1, cout << _1));

如果没有为 ++_1, cout << _1 添加额外的括号,代码将被解释为尝试使用四个参数调用 for_each

由逗号运算符创建的 Lambda 仿函数遵循 C++ 始终先评估左操作数再评估右操作数的规则。在上例中,a 的每个元素都会先被递增,然后写入流。

函数调用运算符

函数调用运算符的效果是求值 Lambda 仿函数。参数不足的调用会导致编译时错误。

成员指针运算符

成员指针运算符 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 引用的形式传递,并作为 const 副本存储在 Lambda 仿函数中。这会在 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

为了防止复制发生,可以使用 refcref 包装器(varconstant_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 具有此能力来处理特定函数对象类。

result_type typedef

BLL 支持标准库的约定,即在函数对象类中通过名为 result_type 的成员 typedef 来声明函数对象的返回类型。这是一个简单的例子:

struct A {
  typedef B result_type;
  B operator()(X, Y, Z); 
};

如果函数对象没有定义 result_type typedef,则会尝试使用下面描述的方法(sig 模板)来解析函数对象的返回类型。如果函数对象同时定义了 result_typesig,则 result_type 具有优先权。

sig 模板

另一种使 BLL 了解函数对象返回类型的方法是定义成员模板结构 sig<Args>,其中包含一个 typedef type 来指定返回类型。这是一个简单的例子:

struct A {
  template <class Args> struct sig { typedef B type; }
  B operator()(X, Y, Z); 
};

模板参数 Args 是一个 tuple(更准确地说,是一个 cons 列表)类型,[tuple],其中第一个元素是函数对象类型本身,其余元素是函数对象被调用时使用的参数的类型。与仅使用简单的 typedef 来表示返回类型相比,这似乎过于复杂。然而,使用简单的 typedef 来表示返回类型存在两个重大限制:

  1. 如果函数对象定义了多个函数调用运算符,则无法为它们指定不同的结果类型。

  2. 如果函数调用运算符是模板,则结果类型可能取决于模板参数。因此,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 和非 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 具有相同的类型,那么扩展返回类型推导(参见 “扩展返回类型推导系统” 一节)是值得的。

零元 Lambda 仿函数和 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<T>(arg-list) 创建的 Lambda 仿函数具有完全相同的功能——除了对于某些零元 Lambda 仿函数,前者无效而后者有效。

延迟常量和变量

一元函数 constantconstant_refvar 将它们的参数转换为 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 类似,但它存储其参数的 const 引用。constantconstant_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 只递增一次,并且其值仅写入输出流一次。通过使用 varindex 转换为 Lambda 表达式,可以获得所需的效果。

总而言之,var(x) 创建一个零元 Lambda 仿函数,该仿函数存储对变量 x 的引用。调用 Lambda 仿函数时,会返回对 x 的引用。

命名延迟常量和变量

可以在 Lambda 表达式外部预定义和命名延迟变量或常量。模板 var_typeconstant_typeconstant_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 = yx[y] 形式的表达式被解释为 Lambda 表达式,左侧操作数 x 必须是一个 Lambda 表达式。因此,有时需要为此目的使用 var。我们重复 “赋值和下标运算符” 一节的示例:

int i; 
i = _1;       // error
var(i) = _1;  // ok

请注意,复合赋值运算符 +=-= 等可以定义为非成员函数,因此即使只有右侧操作数是 Lambda 表达式,它们也会被解释为 Lambda 表达式。然而,显式延迟左侧操作数也是完全可以的。例如,i += _1 等同于 var(i) += _1

用于控制结构的 Lambda 表达式

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(...)

if_then_else_return(它包装了对条件运算符的调用)外,所有控制结构 Lambda 仿函数的返回类型均为 void

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 语句

用于 switch 控制结构的 Lambda 表达式更复杂,因为 case 的数量可能不同。switch Lambda 表达式的一般形式是:

switch_statement(condition, 
  case_statement<label>(lambda expression),
  case_statement<label>(lambda expression),
  ...
  default_statement(lambda expression)
)

condition 参数必须是一个创建具有整型返回类型的 Lambda 仿函数的 Lambda 表达式。不同的 case 通过 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 仿函数。通过一元函数 throw_exception 创建用于抛出异常的 Lambda 仿函数。此函数的参数是要抛出的异常,或一个创建要抛出异常的 Lambda 仿函数。通过零元 rethrow 函数创建用于重新抛出异常的 Lambda 仿函数。

用于处理异常的 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())
    )
  )
);

构造和析构

运算符 newdelete 可以被重载,但它们的返回类型是固定的。特别是,返回类型不能是 Lambda 仿函数,这阻止了它们被重载用于 Lambda 表达式。无法获取构造函数的地址,因此构造函数不能用作绑定表达式中的目标函数。析构函数也是如此。为了规避这些限制,BLL 为 newdelete 调用以及构造函数和析构函数定义了包装器类。这些类的实例是函数对象,可以用作绑定表达式的目标函数。例如:

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 表达式中构造函数调用的示例,以下代码从两个容器 xy 中读取整数,将它们组合成对,并将它们插入到第三个容器中:

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>()(arg_list) T(arg_list)
destructor()(a) a.~A(),其中 a 的类型为 A
destructor()(pa) pa->~A(),其中 pa 的类型为 A*
new_ptr<T>()(arg_list) new T(arg_list)
new_array<T>()(sz) new T[sz]
delete_ptr()(p) delete p
delete_array()(p) delete p[]

特殊 Lambda 表达式

防止参数替换

调用 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);

Unlambda

当目标函数是类型依赖于模板参数的变量时,嵌套的绑定表达式可能会意外发生。通常,目标函数可能是函数模板的形式参数。在这种情况下,程序员可能不知道目标函数是 Lambda 仿函数还是非 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。

使用 unlambdanested 函数的编写方式如下:

template<class F>
int nested(const F& f) {
  int x;
  ...
  bind(unlambda(f), _1)(x);
  ...
}

Protect

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 仿函数的实际参数的右值

Lambda 仿函数的实际参数不能是非 const 的右值。这是由于一个故意的设计决策:要么我们有这个限制,要么对实际参数就不会有副作用。有一些方法可以绕过这个限制。我们重复第 “关于 Lambda 仿函数的实际参数” 节中的示例,并列出不同的解决方案:

int i = 1; int j = 2; 
(_1 + _2)(i, j); // ok
(_1 + _2)(1, 2); // error (!)

  1. 如果右值是类类型,则创建右值的函数的返回类型应定义为 const。由于一个不幸的语言限制,这不适用于内置类型,因为内置右值不能被 const 限定。

  2. 如果 Lambda 函数调用是可访问的,则可以使用 make_const 函数来constify右值。例如:

    (_1 + _2)(make_const(1), make_const(2)); // ok
    

    通常,Lambda 函数的调用点位于标准算法函数模板内部,这使得无法使用此解决方案。

  3. 如果以上任一方法都不可行,则可以将 Lambda 表达式包装在 const_parameters 函数中。它会创建一个另一种类型的 Lambda 仿函数,该仿函数以 const 引用的形式接受其参数。例如:

    const_parameters(_1 + _2)(1, 2); // ok
    

    请注意,const_parameters 会使所有参数都变为 const。因此,在参数中有一个非 const 右值,而另一个参数需要作为非 const 引用传递的情况下,无法使用此方法。

  4. 如果以上任一方法都不可行,则仍然有一个解决方案,但遗憾的是它可能会破坏 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_constconst_parameters 的结果不是 Lambda 仿函数,因此不能用作 Lambda 表达式的子表达式。例如:

    break_const(_1 + _2) + _3; // fails.
    const_parameters(_1 + _2) + _3; // fails.
    

    然而,这种代码永远不应该是必需的,因为 BLL 内部会调用子 Lambda 仿函数,并且不受非 const 右值问题的影响。

类型转换、sizeof 和 typeid

类型转换表达式

BLL 为四种类型转换表达式 static_castdynamic_castconst_castreinterpret_cast 定义了对应的版本。BLL 版本类型转换表达式的前缀为 ll_。要转换到的类型作为显式指定的模板参数给出,而唯一参数是从中执行转换的表达式。如果参数是 Lambda 仿函数,则先求值 Lambda 仿函数。例如,以下代码使用 ll_dynamic_cast 来计算容器 aderived 实例的数量:

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)));

Sizeof 和 typeid

这些表达式的 BLL 对等名称为 ll_sizeofll_typeid。两者都接受一个参数,该参数可以是 Lambda 表达式。创建的 Lambda 仿函数包装了 sizeoftypeid 调用,当调用 Lambda 仿函数时,会执行包装的操作。例如:

vector<base*> a; 
...
for_each(a.begin(), a.end(), 
         cout << bind(&type_info::name, ll_typeid(*_1)));

这里 ll_typeid 为每个元素创建一个调用 typeid 的 Lambda 仿函数。typeid 调用的结果是 type_info 类的实例,而绑定表达式创建一个调用该类 name 成员函数的 Lambda 仿函数。

嵌套 STL 算法调用

BLL 将常见的 STL 算法定义为函数对象类,这些类的实例可以用作绑定表达式中的目标函数。例如,以下代码遍历二维数组的元素并计算它们的总和。

int a[100][200];
int sum = 0;

std::for_each(a, a + 100, 
	      bind(ll::for_each(), _1, _1 + 200, protect(sum += _1)));

STL 算法的 BLL 版本是类,它们定义了函数调用运算符(或几个重载的运算符)来调用 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_begincall_end,它们分别包装对容器的 beginend 函数的调用,并返回容器的 const_iterator 类型。使用这些辅助模板,上述代码变为:

std::for_each(a.begin(), a.end(), 
	      bind(ll::for_each(), 
                   bind(call_begin(), _1), bind(call_end(), _1),
                        protect(sum += _1)));


PrevUpHomeNext