1. 程式人生 > >Boost.Lambda 用法詳解(一)

Boost.Lambda 用法詳解(一)

與其它許多 Boost 庫一樣,這個庫完全定義在標頭檔案中,這意味著你不必構建任何東西就可以開始使用。但是,知道一點關於 lambda 表示式的東西肯定是有幫助的。接下來的章節會帶你瀏覽一下這個庫,還包括如何在 lambda 表示式中進行異常處理!這個庫非常廣泛,前面還有很多強大的東西。一個 lambda 表示式通常也稱為匿名函式(unnamed function)。它在需要的時 候進行宣告和定義,即就地進行。這非常有用,因為我們常常需要在一個演算法中定義另一個演算法,這是語言本身所不能支援的。作為替代,我們通過從更大的範圍引 進函式和函式物件來具體定義行為,或者使用巢狀的迴圈結構,把演算法表示式寫入迴圈中。我們將看到,這正是 lambda

表示式可以發揮的地方。本節內有許多例子,通常例子的一部分是示範如何用"傳統"的編碼方法來解決問題。這樣做的目的是,看看 lambda 表示式在何時以及如何幫助程式寫出更具邏輯且更少的程式碼。使用 lambda 表示式的確存在一定的學習曲線,而且它的語法初看起來有點可怕。就象每種新的正規化或工具,它們都需要去學習,但是請相信我,得到的好處肯定超過付出的代 價。

一個簡單的開始

第一個使用 Boost.Lambda 的程式將會提升你對 lambda 表示式的喜愛。首先,請注意 lambda 型別是宣告在 boost::lambda 名字空間中,你需要用一個 using 指示符或 using 宣告來把這些 lambda

宣告帶入你的作用域。包含標頭檔案 "boost/lambda/lambda.hpp" 就可以使用這個庫的主要功能了,對於我們第一個程式這樣已經足夠了。

#include <iostream>
#include "boost/lambda/lambda.hpp"
#include "boost/function.hpp"
int main() {
  using namespace boost::lambda;
  (std::cout << _1 << " " << _3 << " " << _2 << "!\n")
    ("Hello","friend","my");
boost::function<void(int,int,int)> f= std::cout << _1 << "*" << _2 << "+" << _3 << "=" <<_1*_2+_3 << "\n"; f(1,2,3); f(3,2,1); }

第一個表示式看起來很奇特,你可以在腦子裡按著括號來劃分這個表示式;第一部分就是一個 lambda 表示式,它的意思基本上是說,"列印這些引數到 std::cout, 但不是立即就做,因為我還不知道這三個引數"。表示式的第二部分才是真正呼叫這個函式,它說,"嘿!這裡有你要的三個引數"。我們再來看看這個表示式的第一部分。

std::cout << _1 << " " << _3 << " " << _2 << "!\n"

你會注意到表示式中有三個佔位符,命名為 _1, _2, _3 [1]。 這些佔位符為 lambda 表示式指出了延後的引數。注意,跟許多函數語言程式設計語言的語法不一樣,建立 lambda 表示式時沒有關鍵字或名字;佔位符的出現表明了這是一個 lambda 表示式。所以,這是一個接受三個引數的 lambda 表示式,引數的型別可以是任何支援 operator<< 流操作的型別。引數按 1-3-2 的順序列印到 cout 。在這個例子中,我們把這個表示式用括號括起來,然後呼叫得到的這個函式物件,傳遞三個引數給它:"Hello", "friend", "my". 輸出的結果如下:

[1]你可能沒想到象 _1 這樣的識別符號是合法的,但它們的確是。識別符號不能由數字打頭,但可以由下劃線打頭,而數字可以出現在識別符號的其它任何地方。

Hello my friend!

通常,我們要把函式物件傳入演算法,這是我們要進一步研究的,但是我們來試驗一些更有用的東西,把 lambda 表示式存入另一個延後呼叫的函式,名為 boost::function. 這個有用的發明將在下一章 中討論,現在你只有知道可以傳遞一個函式或函式物件給 boost::function 的例項並儲存它以備後用就可以了。在本例中,我們定義了這樣的一個函式 f,象這樣:

boost::function<void(int,int,int)> f;

這個宣告表示 f 可以存放用三個引數呼叫的函式和函式物件,引數的型別全部為 int. 然後,我們用一個 lambda 表示式把一個函式物件賦給它,這個表示式表示了演算法 X=S*T+U, 並且把這個算式及其結果列印到 cout.

boost::function<void(int,int,int)> f=
  std::cout <<
    _1 << "*" << _2 << "+" << _3 << "=" <<_1*_2+_3 << "\n";

如你所見,在一個表示式中,佔位符可以多次使用。我們的函式 f 現在可以象一個普通函式那樣呼叫了,如下:

f(1,2,3);
f(3,2,1);

執行這段程式碼的輸出如下。

1*2+3=5
3*2+1=7

任意使用標準操作符(操作符還可以被過載!)的表示式都可以用在 lambda 表示式中,並可以儲存下來以後呼叫,或者直接傳遞給某個演算法。你要留意,當一個 lambda 表示式沒有使用佔位符時(我們還沒有看到如何實現,但的確可以這樣用),那麼結果將是一個無參函式(物件)。作為對比,只使用 _1 時,結果是一個單引數函式物件;只使用 _1 _2 時,結果則是一個二元函式物件;當只使用 _1, _2, _3 時,結果就是一個三元函式物件。這第一個 lambda 表示式受益於這樣一個事實,即該表示式只使用了內建或常用的C++操作符,這樣就可以直接編寫演算法。接下來,我們看看如何繫結表示式到其它函式、類成員函式,甚至是資料成員!

在操作符不夠用時就用繫結

到目前為止,我們已經看到如果有操作符可以支援我們的表示式,一切順利,但並不總是如此的。有時我們需要把呼叫另一個函式作為表示式的一部分,這通常要藉助於繫結;這種繫結與我們前面在建立 lambda 表示式時見過的繫結有所不同,它需要一個單獨的關鍵字,bind (嘿,這真是個聰明的名字!)。一個繫結表示式就是一個被延遲的函式呼叫,可以是普通函式或成員函式。該函式可以有零個或多個引數,某些引數可以直接設定,另一些則可以在函式呼叫時給出。對於當前版本的 Boost.Lambda, 最多可支援九個引數(其中三個可以通過使用佔位符在稍後給出)。要使用繫結器,你需要包含標頭檔案"boost/lambda/bind.hpp"

在繫結到一個函式時,第一個引數就是該函式的地址,後面的引數則是函式的引數。對於一個非靜態成員函式,總是有一個隱式的 this 引數;在一個 bind 表示式中,必須顯式地加上 this 引數。為方便起見,不論物件是通過引用傳遞或是通過指標傳遞,語法都是一樣的。因此,在繫結到一個成員函式時,第二個引數(即函式指標後的第一個)就是將要呼叫該函式的真實物件。繫結到資料成員也是可以的,下面的例子也將有所示範:

#include <iostream>
#include <string>
#include <map>
#include <algorithm>
#include "boost/lambda/lambda.hpp"
#include "boost/lambda/bind.hpp"
int main() {
  using namespace boost::lambda;
  typedef std::map<int,std::string> type;
  type keys_and_values;
  keys_and_values[3]="Less than pi";
  keys_and_values[42]="You tell me";
  keys_and_values[0]="Nothing, if you ask me";
  std::cout << "What's wrong with the following expression?\n";
  std::for_each(
    keys_and_values.begin(),
    keys_and_values.end(),
    std::cout << "key=" <<
      bind(&type::value_type::first,_1) << ", value="
      << bind(&type::value_type::second,_1) << '\n');
  std::cout << "\n...and why does this work as expected?\n";
  std::for_each(
    keys_and_values.begin(),
    keys_and_values.end(),
    std::cout << constant("key=") <<
      bind(&type::value_type::first,_1) << ", value="
      << bind(&type::value_type::second,_1) << '\n');
  std::cout << '\n';
  // Print the size and max_size of the container
  (std::cout << "keys_and_values.size()=" <<
    bind(&type::size,_1) << "\nkeys_and_values.max_size()="
    << bind(&type::max_size,_1))(keys_and_values);
}

這個例子開始時先建立一個 std::map ,鍵型別為 int 且值型別為 std::string 。記住,std::map value_type 是一個由鍵型別和值型別組成的 std::pair 。因此,對於我們的 map, value_type 就是 std::pair<int,std::string>, 所以在 for_each 演算法中,我們傳入的函式物件要接受一個這樣的型別。給出這個 pair, 就可以取出其中的兩個成員(鍵和值),這正是我們第一個 bind 表示式所做的。

bind(&type::value_type::first,_1)

這個表示式生成一個函式物件,它被呼叫時將取出它的引數,即我們前面討論的 pair 中的巢狀型別 value_type 的資料成員 first。在我們的例子中,first map 的鍵型別,它是一個 const int. 對於成員函式,語法是完全相同的。但你要留意,我們的 lambda 表示式多做了一點;表示式的第一部分是

std::cout << "key=" << ...

它可以編譯,也可以工作,但它可能不能達到目的。這個表示式不是一個 lambda 表示式;它只是一個表示式而已,再沒有別的了。執行時,它列印 key=, 當這個表示式被求值時它僅執行一次,而不是對於每個被 std::for_each 所訪問的元素執行一次。在這個例子中,原意是把 key= 作為我們的每一個 keys_and_values /值對的字首。在早一點的那些例子中,我們也是這樣寫的,但那裡沒有出現這些問題。原因在於,那裡我們用了一個佔位符來作為 operator<< 的第一個引數,這樣就使得它成為一個有效的 lambda 表示式。而這裡,我們必須告訴 Boost.Lambda 要建立一個包含 "key=" 的函式物件。這就要使用函式 constant, 它建立一個無參函式物件,即不帶引數的函式物件;它僅僅儲存其引數,然後在被呼叫時返回它。

std::cout << constant("key=") << ...

這個小小的修改使得所有輸出都不一樣了,以下是該程式的執行輸出結果。

What's wrong with the following expression?
key=0, value=Nothing, if you ask me
3, value=Less than pi
42, value=You tell me
...and why does this work as expected?
key=0, value=Nothing, if you ask me
key=3, value=Less than pi
key=42, value=You tell me
keys_and_values.size()=3
keys_and_values.max_size()=4294967295

例子的最後一部分是一個繫結到成員函式的繫結器,而不是繫結到資料成員;語法是一樣的,而且你可以看 到在這兩種情形下,都不需要顯式地表明函式的返回型別。這種奇妙的事情是由於函式或成員函式的返回型別可以被自動推斷,如果是繫結到資料成員,其型別同樣 可以自動得到。但是,有一種情形不能得到返回型別,即當被繫結的是函式物件時;對於普通函式和成員函式,推斷其返回型別是一件簡單的事情[2],但對於函式物件則不可能。有兩種方法繞過這個語言的限制,第一種是由 Lambda 庫自己來解決:通過顯式地給出 bind 的模板引數來替代返回型別推斷,如下所示。

[2]你也得小心行事。我們只是說它在技術上可行。

class double_it {
public:
  int operator()(int i) const {
    return i*2;
  }
};
int main() {
  using namespace boost::lambda;
  double_it d;
  int i=12;
  // If you uncomment the following expression,
  // the compiler will complain;
  // it's just not possible to deduce the return type
  // of the function call operator of double_it.
  // (std::cout << _1 << "*2=" << (bind(d,_1)))(i);
  (std::cout << _1 << "*2=" << (bind<int>(d,_1)))(i);
  (std::cout << _1 << "*2=" << (ret<int>(bind(d,_1))))(i);
}

有兩種版本的方法來關閉返回型別推斷系統,短格式的版本只需把返回型別作為模板引數傳給 bind, 另一個版本則使用 ret, 它要括住不能進行自動推斷的 lambda/bind 表示式。在巢狀的 lambda 表示式中,這很容易會就得乏味,不過還有一種更好的方法可以讓推斷成功。我們將在本章稍後進行介紹。

請注意,一個繫結表示式可以由另一個繫結表示式組成,這使得繫結器成為了進行函式組合的強大工具。巢狀的繫結有許多強大的功能,但是要小心使用,因為這些強大的功能同時也帶來了讀寫以及理解程式碼上的額外的複雜性。

我不喜歡 _1, _2, and _3,我可以用別的名字嗎?

有的人對預定義的佔位符名稱不滿意,因此本庫提供了簡便的方法來把它們[3]改為任意使用者想用的名字。這是通過宣告一些型別為 boost::lambda::placeholderX_type 的變數來實現的,其中 X 1, 2, 3. 例如,假設某人喜歡用 Arg1, Arg2, Arg3 來作佔位符的名字:

[3]技術上,是增加新的名字。

#include <iostream>
#include <vector>
#include <string>
#include "boost/lambda/lambda.hpp"
boost::lambda::placeholder1_type Arg1;
boost::lambda::placeholder2_type Arg2;
boost::lambda::placeholder3_type Arg3;
template <typename T,typename Operation>
void for_all(T& t,Operation Op) {
  std::for_each(t.begin(),t.end(),Op);
}
int main() {
  std::vector<std::string> vec;
  vec.push_back("What are");
  vec.push_back("the names");
  vec.push_back("of the");
  vec.push_back("placeholders?");
  for_all(vec,std::cout << Arg1 << " ");
  std::cout << "\nArg1, Arg2, and Arg3!";
}

你定義的佔位符變數可以象 _1, _2, _3 一樣使用。另外請注意這裡的函式 for_all ,它提供了一個簡便的方法,當你經常要對一個容器中的所有元素進行操作時,可以比用 for_each 減少一些鍵擊次數。這個函式接受兩個引數:一個容器的引用,以及一個函式或函式物件。該容器中的每個元素將被提供給這個函式或函式物件。我認為它有時會非常有用,也許你也這樣認為。執行這個程式將產生以下輸出:

What are the names of the placeholders?
Arg1, Arg2, and Arg3!

建立你自己的佔位符可能會影響其它閱讀你的程式碼的人;多數知道 Boost.Lambda ( Boost.Bind) 的程式設計師都熟悉佔位符名稱 _1, _2, _3. 如果你決定把它們稱為 q, w, e, 你就需要解釋給你的同事聽它們有什麼意思。(而且你可能要經常重複地進行解釋!)

我想給我的常量和變數命名!

有時,給常量和變數命名可以提高程式碼的可讀性。你也記得,我們有時需要建立一個不是立即求值的 lambda 表示式。這時可以使用 constant var; 它們分別對應於常量或變數。我們已經用過 constant 了,基本上 var 也是相同的用法。對於複雜或長一些的 lambda 表示式,對一個或多個常量給出名字可以使得表示式更易於理解;對於變數也是如此。要建立命名的常量和變數,你只需要定義一個型別為 boost::lambda::constant_type<T>::type boost::lambda::var_type<T>::type 的變數,其中的 T 為被包裝的常量或變數的型別。看一下以下這個 lambda 表示式的用法:

for_all(vec,
  std::cout << constant(' ') << _ << constant('\n'));

總是使用 constant 會很讓人討厭。下面是一個例子,它命名了兩個常量,newline space,並把它們用於 lambda 表示式。

#include <iostream>
#include <vector>
#include <algorithm>
#include "boost/lambda/lambda.hpp"
int main() {
  using boost::lambda::constant;
  using boost::lambda::constant_type;
  constant_type<char>::type newline(constant('\n'));
  constant_type<char>::type space(constant(' '));
  boost::lambda::placeholder1_type _;
  std::vector<int> vec;
  vec.push_back(0);
  vec.push_back(1);
  vec.push_back(2);
  vec.push_back(3);
  vec.push_back(4);
  for_all(vec,std::cout << space << _ << newline);
  for_all(vec,
    std::cout << constant(' ') << _ << constant('\n'));
}

這是一個避免重複鍵入的好方法,也可以使 lambda 表示式更清楚些。下面是一個類似的例子,首先定義一個型別 memorizer, 用於跟蹤曾經賦給它的所有值。然後,用 var_type 建立一個命名變數,用於後面的 lambda 表示式。你將會看到命名常量要比命名變數更常用到,但也有些情形會需要使用命名變數[4]

[4]特別是使用 lambda 迴圈結構時。

#include <iostream>
#include <vector>
#include <algorithm>
#include "boost/lambda/lambda.hpp"
template <typename T> class memorizer {
  std::vector<T> vec_;
public:
  memorizer& operator=(const T& t) {
    vec_.push_back(t);
    return *this;
  }
  void clear() {
    vec_.clear();
  }
  void report() const {
    using boost::lambda::_1;
    std::for_each(
      vec_.begin(),
      vec_.end(),
      std::cout << _1 << ",");
  }
};
int main() {
  using boost::lambda::var_type;
  using boost::lambda::var;
  using boost::lambda::_1;
  std::vector<int> vec;
  vec.push_back(0);
  vec.push_back(1);
  vec.push_back(2);
  vec.push_back(3);
  vec.push_back(4);
  memorizer<int> m;
  var_type<memorizer<int> >::type mem(var(m));
  std::for_each(vec.begin(),vec.end(),mem=_1);
  m.report();
  m.clear();
  std::for_each(vec.begin(),vec.end(),var(m)=_1);
  m.report();
}

這就是它的全部了,但在你認為自己已經明白了所有東西之前,先回答這個問題:在以下宣告下 T 應該是什麼型別?

constant_type<T>::type hello(constant("Hello"));

它是一個 char*? 一個 const char*? 都不是,它的正確型別是一個含有六個字元(還有一個結束用的空字元)的陣列的常量引用,所以我們應該這樣寫:

constant_type<const char (&)[6]>::type
  hello(constant("Hello"));

這很不好看,而且對於需要修改這個字串的人來說也很痛苦,所以我更願意使用 std::string 來寫。

constant_type<std::string>::type
  hello_string(constant(std::string("Hello")));

這次,你需要比上一次多敲幾個字,但你不需要再計算字元的個數,如果你要改變這個字串,也沒有問題。

ptr_fun mem_fun 到哪去了?

也許你還在懷念它們,由於 Boost.Lambda 建立了與標準一致的函式物件,所以沒有必要再記住這些標準庫中的介面卡型別了。一個綁定了函式或成員函式的 lambda 表示式可以很好地工作,而且不論繫結的是什麼型別,其語法都是一致的。這可以讓程式碼更注重其任務而不是某些奇特的語法。以下例子說明了這些好處:

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
#include "boost/lambda/lambda.hpp"
#include "boost/lambda/bind.hpp"
void plain_function(int i) {
  std::cout << "void plain_function(" << i << ")\n";
}
class some_class {
public:
  void member_function(int i) const {
    std::cout <<
      "void some_class::member_function(" << i << ") const\n";
  }
};
int main() {
  std::vector<int> vec(3);
  vec[0]=12;
  vec[1]=10;
  vec[2]=7;
  some_class sc;
  some_class* psc=&sc;
  // Bind to a free function using ptr_fun
  std::for_each(
    vec.begin(),
    vec.end(),
    std::ptr_fun(plain_function));
  // Bind to a member function using mem_fun_ref
  std::for_each(vec.begin(),vec.end(),
    std::bind1st(
      std::mem_fun_ref(&some_class::member_function),sc));
  // Bind to a member function using mem_fun
  std::for_each(vec.begin(),vec.end(),
    std::bind1st(
      std::mem_fun(&some_class::member_function),psc));
  using namespace boost::lambda;
  std::for_each(
    vec.begin(),
    vec.end(),
    bind(&plain_function,_1));
  std::for_each(vec.begin(),vec.end(),
    bind(&some_class::member_function,sc,_1));
  std::for_each(vec.begin(),vec.end(),
    bind(&some_class::member_function,psc,_1));
}

這裡真的不需要用 lambda 表示式嗎?相對於使用三個不同的結構來完成同一件事情,我們可以只需向 bind 指出要幹什麼,然後它就會去做。在這個例子中,需要用 std::bind1st 來把 some_class 的例項繫結到呼叫中;而對於 Boost.Lambda,這是它工作的一部分。因此,下次你再想是否要用 ptr_fun, mem_fun, mem_fun_ref 時,停下來,使用 Boost.Lambda 來代替它!

無須<functional>的算術操作

我們常常要按順序對一些元素執行算術操作,而標準庫提供了多個函式物件來執行算術操作,如 plus, minus, divides, modulus, 等等。但是,這些函式物件需要我們多打很多字,而且常常需要繫結一個引數,這時應該使用繫結器。如果要巢狀這些算術操作,表示式很快就會變得難以使用,而 這正是 lambda 表示式可以發揮巨大作用的地方。因為我們正在處理的是操作符,既是算術上的也是C++術語上的,所以我們有能力使用 lambda 表示式直接編寫我們的演算法程式碼。作為一個小的動機,考慮一個簡單的問題,對一個數值增加4。然後再考慮另一個問題,完成與標準庫演算法(transform)同樣的工作。雖然第一個問題非常自然,而第二個則完全不一樣(它需要你手工寫迴圈)。但使用 lambda 表示式,只需關注演算法本身。在下例中,我們先使用 std::bind1st std::plus 對容器中的每個元素加4,然後我們使用 lambda 來減4

#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
#include "boost/lambda/lambda.hpp"
#include "boost/lambda/bind.hpp"
int main() {
  using namespace boost::lambda;
  std::vector<int> vec(3);
  vec[0]=12;
  vec[1]=10;
  vec[2]=7;
  // Transform using std::bind1st and std::plus
  std::transform(vec.begin(),vec.end(),vec.begin(),
    std::bind1st(std::plus<int>(),4));
  // Transform using a lambda expression
  std::transform(vec.begin(),vec.end(),vec.begin(),_1-=4);
}

差別是令人驚訝的!在使用"傳統"方法進行加4時,對於未經訓練的眼睛來說,很難看出究竟在幹什麼。從程式碼中我們看到,我們將一個預設構造的 std::plus 例項的第一個引數繫結到4。而 lambda 表示式則寫成從元素減4。如果你認為使用 bind1st plus 的版本還不壞,你可以試試更長的表示式。