Boost C++ Libraries Home Libraries People FAQ More

PrevUpHomeNext

Lambda expressions in details(lambda 表達式詳細研究)

Placeholders(佔位符)
Operator expressions(操作符表達式)
Bind expressions(bind 表達式)
Overriding the deduced return type(越過推演出的返回類型)
Delaying constants and variables(延遲常量和變量)
Lambda expressions for control structures(控制結構的 lambda 表達式)
Exceptions(異常)
Construction and destruction(構造函數和析構函數)
Special lambda expressions(特殊lambda 表達式)
Casts, sizeof and typeid(強制轉型,sizeof 和 typeid)
Nesting STL algorithm invocations(嵌入 STL 算法調用)

本節詳細描述 lambda 表達式的各個不同方面。我們為一個 lambda 表達式的每一種可能的形式都投入一個專門的部分。

Placeholders(佔位符)

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 functor arity」 部分

除了這三個佔位符類型之外,還有第四個佔位符類型 placeholderE_type。這個佔位符的作用是定義 「Exceptions」 部分描述的 lambda 表達式中的異常處理。

為一個佔位符提供真正的參數的時候,參數傳遞的方式總是傳引用。這就意味著任何影響佔位符的副作用都會反映到實際參數上。例如:

int i = 1; 
(_1 += 2)(i); // i is now 3
(++_1, cout << _1)(i) // i is now 4, outputs 4

Operator expressions(操作符表達式)

基本規則是任何 C++ 操作符調用,只要它的參數中至少有一個是 lambda 表達式,則這個調用本身也是 lambda 表達式。幾乎所有的能重載操作符都已被支持。例如,下面就是一個合法的 lambda 表達式:

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

然而,有一些來自於 C++ 操作符重載規則的限制,以及一些特殊情況。

Operators that cannot be overloaded(不能重載的操作符)

有些操作符根本不能重載(::, ., .*)。對於有些操作符,對返回類型的要求阻礙了它們為創建 lambda 函數而重載。這些操作符有 ->., ->, new, new[], delete, delete[]?:(條件操作符)。

Assignment and subscript operators(賦值和下標操作符)

這些操作符必須被實現為類成員。因此,左操作數必須是一個 lambda 表達式。例如:

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

關於這一限制有一個簡單的解決方案,在 「Delaying constants and variables」 部分描述。簡而言之,就是通過用一個特殊的 var 函數進行包裝,左側參數可以被顯式轉變為 lambda 仿函數:

var(i) = _1; // ok

Logical operators(邏輯操作符)

邏輯操作符服從短路求值規則。例如,在下面的代碼中,i 沒有被增加:

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

Comma operator(逗號操作符)

逗號操作符在 lambda 表達式中是「語句分隔符」。因為逗號也是函數調用中的參數分隔符,所以有時需要額外的括號:

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

如果沒有包圍 ++_1, cout << _1 的額外括號,這行代碼的意圖會被解釋為用四個參數調用 for_each

使用逗號操作符建立的 lambda 仿函數堅 C++ 中左操作數的求值總是先於右操作數的規則。在上面的示例中,a 中的每一個元素首先被增 1,然後才寫入流中。

Function call operator(函數調用操作符)

函數調用操作符有求 lambda 仿函數的值的作用。用過多的參數調用會導致一個編譯時錯誤。

Member pointer operator(成員指針操作符)

成員指針操作符 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 expressions(bind 表達式)

bind 表達式可以有兩種形式:

bind(target-function, bind-argument-list)
bind(target-member-function, object-argument, bind-argument-list)

一個 bind 表達式延遲了一個函數的調用。如果這個 target functionn 元的,那麼 bind-argument-list 也必須同樣包含 n 個參數。在 BLL 的當前版本中,必須保證 0 <= n <= 9。對於成員函數來說,參數的數目最高為 8,因為對像參數要佔有一個參數位置。總的來說,除了任何一個參數都能被一個佔位符(更一般地說,是 lambda 表達式)取代之外,還要求 bind-argument-list 對於目標函數來說必須是一個合法的參數列表。注意,目標函數也可以是一個 lambda 表達式。根據在 bind-argument-list 中佔位符的使用,一個 bind 表達式的結果可以是無元的,一元的,二元的或三元的函數對像(參見 「Placeholders」 部分)。

bind 表達式創建的 lambda 仿函數的返回類型可以由顯式特化的模板參數給定,就像下面的示例:

bind<RET>(target-function, bind-argument-list)

這一方法只有在目標函數的返回類型不能被推演出來的情況下才是必要的。

以下部分描述 bind 表達式的不同類型。

Function pointers or references as targets(以函數指針或引用為目標)

目標函數可以是指向一個函數的指針或引向一個函數的引用,而且它可以使已被綁定的或未被綁定的。例如:

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

這種類型的 bind 表達式的返回類型推演總是能夠成功。

注意,在 C++ 中,只有當一個重載函數的地址符合以下條件,才可能持有這個地址,它被賦值給一個變量,或用於初始化一個變量,這個變量的類型消除了多義性,或者使用了一個顯式的強制轉型表達式。這就意味著重載函數不能直接用於一個 bind 表達式,例如:

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

Member functions as targets(以成員函數為目標)

在 bind 表達式內使用指向成員函數的指針的語法為:

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-functions 持有它們的參數的方法是不同的,以及被綁定參數是如何存儲在 lambda 仿函數中的。對像參數與任何其它 bind 參數具有相同的參數傳遞和存儲機制(參見 「Storing bound arguments in lambda functions」 部分),它以一個常引用傳遞並以一個常拷貝存儲在 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

儘管存儲的是一個對像參數的常拷貝,原始對像 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

Member variables as targets(以成員變量為目標)

一個指向成員變量的指針不是一個真正的函數,但是 bind 函數的第一個參數依然可以是一個指向成員變量的指針。調用這樣一個 bind 表達式會返回一個引向這個數據成員的引用。例如:

struct A { int data; };
A a;
bind(&A::data, _1)(a) = 1; // a.data == 1

成員被訪問的那個對象的 cv 修飾符(c 為 const,v 為 volatile ——譯者注)也需要被考慮。例如,下面的例子就是試圖寫到一個 const 區域中:

const A ca = a;
bind(&A::data, _1)(ca) = 1; // error

Function objects as targets(以函數對像為目標)

函數對象,換句話說,就是定義了函數調用操作符,可以像目標函數一樣使用的類對象。通常,BLL 不能推演一個任意的函數對象的返回類型。然而,有兩種方法賦予 BLL 這種針對某一函數對像類的能力。

The result_type typedef(結果類型 typedef)

BLL 支持標準庫的在一個函數對像類中用一個名為 result_type 的成員 typedef 聲明一個函數對象的返回類型的慣例。這是一個簡單的示例:

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

如果一個函數對像沒有定義 result_type typedef,下面描述的方法(sig 模板)也可以用來決定函數對象的返回類型。如果一個函數對像既定義了 result_type 也定義了 sig,優先使用 result_type

The sig template(sig 模板)

另一種可以讓 BLL 感知一個函數對象的返回類型的機制是定義一個成員模板結構 sig<Args>,其中包含一個指定返回類型的 typedef type。這是一個簡單的示例:

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

模板參數 Args 是一個 tuple(或者更精確些,一個 cons list(部件鏈表))類型 [tuple],它的第一個元素是函數對像類型本身,而剩下的元素是調用這個函數對象的參數的類型。這與 定義一個 result_type 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-qualifiers(cv 修飾符)),或者兩者都有。這是因為參數中的 cv 修飾符會影響返回類型。將潛在的被 cv 修飾過的函數返回類型本身包含到 Args tuple 中的理由,在於函數對像類可以包含 const 和非 const(或者 volatile,甚至 const volatile)的函數調用操作符,而且它們每一個都會有一個不同的返回類型。

sig 模板可以被看做一個 meta-function(元 函數),它使用 tuple 中的參數類型將參數類型 tuple 映射出為調用的返回類型。從上面的示例看來,那個模板最終變得有些複雜。它所執行的典型任務就是從 tuple 中提取相關類型,移除 cv 修飾符等等。對於這些能幫助這個任務的工具可以參見 Boost type_traits [type_traits] 和 Tuple [type_traits] 庫。sig 模板是一個最初在 FC++ 庫 [fc++] 中引入的類似機制的一個優雅的版本。

Overriding the deduced return type(越過推演出的返回類型)

返回類型推演系統可能不能推演某些用戶定義操作符或帶有類對象的 bind 表達式的返回類型。有一種特殊的 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

對於 bind 表達式,有一個簡單的記法用來代替 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,那就值得擴展返回類型推演(參見 「Extending return type deduction system」 部分)。

Nullary lambda functors and ret(無元 lambda 仿函數和 ret)

就像上面所表述的,ret 的作用是阻止執行返回類型推演。然而,有一個例外。由於 C++ 模板實例化的工作方式,對於 0 參數的 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 不能推演出上面的 bind 調用的返回類型,因為 F 沒有定義 typedef result_type。有人希望用 ret 來修復它,但是對於無元 lambda 仿函數來說,來自於 bind 表達式(上面最後一行)的結果不能工作。返回類型推演模板已經被實例化,即使它沒有必要而結果是一個編譯錯誤。

這個問題的解決方案是不使用 ret 函數,而是將返回類型定義成 bind 調用中的一個 explicitly specified template parameter(顯式特化的模板參數):

bind<int>(f, 1);       // ok

使用 ret<T>(bind(arg-list))bind<T>(arg-list) 創建的仿函數具有完全相同的功能——實際上的區別是對於某些無元 lambda 仿函數,後者可以工作,而前者不行。

Delaying constants and variables(延遲常量和變量)

一元函數 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 與此類似,只不過它存儲的是它的參數的一個 constant reference(常引用)。constantconsant_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 的引用。

Naming delayed constants and variables(有名字的延遲常量和變量)

在一個 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 of type T 的一個延遲版本。類似地,第二行定義的常量 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);

About assignment and subscript operators(關於賦值和下標操作符)

就像在 「Assignment and subscript operators」 部分描述的,賦值和下標操作符總是定義為成員函數。這就意味著,為了把 x = yx[y] 形式的表達式解釋為 lambda 表達式,左側操作數 x 必須是一個 lambda 表達式。因此,有時需要用 var 來達到這個目的。我們再看一看 「Assignment and subscript operators」 部分的示例:

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

注意,混合式賦值操作符 +=-= 等可以定義為非成員函數,因此即使只有右側操作數是一個 lambda 表達式,它們也可以被解釋為 lambda 表達式。不過,延遲左操作數還是非常完美的。例如,i += _1 等價於 var(i) += _1

Lambda expressions for control structures(控制結構的 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(...)

所有控制結構 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 statement(switch 語句)

switch 控制結構的 lambda 表達式更加複雜,因為 cases 的數量是可以變化的。一個 switch lambda 表達式的常規形式是:

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

condition 參數必須是一個創建帶有一個整型返回值的 lambda 仿函數的 lambda 表達式。各種不同的 cases 通過 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」。注意,依次排列在 switch_statement 後面的另一個 lambda 表達式在每一個元素後輸出一行 break:

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

Exceptions (異常)

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>(...) 導致的 chatch 塊如下:

catch(T& e) { ... }

最後一個 chatch 塊可以在 catch_exception<type>catch_all(這一 lambda 表達式等價於 catch(...))這兩個調用之中選擇一個。

Example 8.1 「在 lambda 表達式中拋出和處理異常」示範了 BLL 的異常處理工具的使用。第一個 handler(處理器)捕捉 foo_exception 類型的異常。注意 handler(處理器)體內 _1 佔位符的使用。

第二個 handler(處理器)展示如何拋出異常,並示範了 exception placeholder(異常佔位符)_e 的使用。這是一個特殊的佔位符,它與在 handler(處理器)體內捕捉到的異常對像相關。這裡我們處理一個類型為 std::exception 的異常,它帶有一個解釋異常原因的字符串。這一解釋可以用無參數成員函數 what 進行查詢。表達式 bind(&std::exception::what, _e) 創建能產生這個調用的 lambda 函數。注意,_e 不能在一個異常處理器 lambda 表達式之外使用。第二個 handler(處理器)的最後一行構造一個新的異常對象並通過 throw exception 將它拋出。在 lambda 表達式中構造和析構對像在 「Construction and destruction」 部分進行了說明。

最後,第三個 handler(處理器)(catch_all) 示範重新拋出異常。

Example 8.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())
)
)
);

Construction and destruction(構造函數和析構函數)

操作符 newdelete 能被重載,但它們的返回類型是固定的。特別是,返回類型不能是 lambda 表達式,以防止它們為 lambda 表達式重載。持有一個構造函數的地址是不可能的,因此構造函數不能在 bind 表達式中作為目標函數使用。這些對於析構函數也同樣成立。為了繞過這些限制,BLL 提供了 newdelete 的包裝類,也提供了構造函數和析構函數的包裝類。這些函數的實例是函數對象,它們可以在bind 表達式中作為目標函數使用。例如:

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 中讀取整數,用這兩個整數創建 pairs,並把它們插入到第三個容器中:

vector<pair<int, int> > v;
transform(x.begin(), x.end(), y.begin(), back_inserter(v),
bind(constructor<pair<int, int> >(), _1, _2));

Table 11.1 「構造函數和析構函數相關函數對像」列出了所有與創建和銷毀對像相關的函數對象,展示了被創建的表達式和調用的函數對象,以及對表達式求值的效果。

Table 11.1 構造函數和析構函數相關函數對像

函數對像調用 被包裝表達式
constructor<T>()(arg_list) T(arg_list)
destructor()(a) a.~A(), where a is of type A
destructor()(pa) pa->~A(), where pa is of type 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[]

Special lambda expressions(特殊 lambda 表達式)

Preventing argument substitution(阻止參數置換)

當一個 lambda 仿函數被調用的時候,缺省行為是在所有子表達式內用實際參數替換佔位符。本節描述阻止這種替換和和子表達式求值的工具,並說明這些工具應該在什麼時候使用。

一個 bind 表達式的參數可以是任意的 lambda 表達式,比如,其它的 bind 表達式。例如:

int foo(int); int bar(int);
...
int i;
bind(foo, bind(bar, _1)(i);

最後一行調用了 foo(bar(i));,注意,bind 表達式中的第一個參數,也就是目標函數,也不例外,因此也可以是一個 bind 表達式。只不過最裡層的 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

某種嵌套的 bind 表達式可能會在漫不經心中出現,它的目標函數是一個變量,這個變量的類型依賴於一個模板參數。特別是目標函數可以是一個函數模板的形式參數。在這種情況下,程序員可能不知道這個目標函數是否是一個 lambda 仿函數。

考慮下面的函數模板:

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

在函數內的某處,形式參數 f 被用作一個 bind 表達式的目標函數。為了使這個 bind 調用合法,f 必須是一個一元函數。假設有下面兩個對 nested 的調用:

int foo(int);
int bar(int, int);
nested(&foo);
nested(bind(bar, 1, _1));

都是帶有恰當的參數和返回類型的一元函數,或者函數對象,但是後一個無法正常編譯。在後一個調用中,nested 中的 bind 表達式會變成:

bind(bind(bar, 1, _1), _1) 

當用 x 來調用它時,替換之後我們最終企圖調用

bar(1, x)(x)

這是一個錯誤,對 bar 的調用返回 int,不是一個一元函數或函數對象。

在上面的例子中,nested 函數內的 bind 表達式的意圖是將 f 看做一個普通的函數對象。BLL 提供函數模板 unlambda 來表達這一點:一個包裝在 unlambda 中的 lambda 仿函數不再是一個 lambda 仿函數,也不再參與參數替換過程。注意,對於所有其它參數類型而言,除了將非 const 對像變成 const 之外,unlambda 是一個恆等操作。

使用 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 算法調用(「Nesting STL algorithm invocations」 部分)。

Rvalues as actual arguments to lambda functors(右值作為 lambda 仿函數的實際參數)

lambda 仿函數的參數不能是非 const 右值。這是因為一個經過深思熟慮的設計權衡:我們有了這個約束,就不會對實際參數產生副作用。也有方法可以繞過這個限制。我們再看一下 「About actual arguments to lambda functors」 部分 的例子,並列舉不同的解決方案:

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 仿函數,這個 lambda 仿函數以 const 方式持有它的參數,並在調用原來的被包裝的 lambda 仿函數之前強行去掉它的常量性。例如:

    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.

    但是,這種代碼應該永遠不是必要的,因為調用子 lambda 仿函數的方法已經做入到 BLL 中,而且它不會受到非 const 右值問題的影響。

Casts, sizeof and typeid(強制轉型,sizeof 和 typeid)

Cast expressions(強制轉型表達式)

BLL 定義了針對四種強制轉型表達式 static_cast, dynamic_cast, const_cast and reinterpret_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 and 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 創建一個 lambda 仿函數用於對每一個元素調用 typeid。一個 typeid 調用的結果是一個 type_info 類的實例,而那個 bind 表達式創建一個 lambda 表達式用於調用那個類的 name 成員函數。

Nesting STL algorithm invocations(嵌入 STL 算法調用)

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

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

Copyright 1999-2004 Jaakko Jrvi, Gary Powell

PrevUpHomeNext